| /* |
| Copyright 2016 Google LLC |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| https://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| // Buildozer is a tool for programmatically editing BUILD files. |
| |
| package edit |
| |
| import ( |
| "bufio" |
| "bytes" |
| "errors" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| |
| apipb "github.com/bazelbuild/buildtools/api_proto" |
| "github.com/bazelbuild/buildtools/build" |
| "github.com/bazelbuild/buildtools/edit/bzlmod" |
| "github.com/bazelbuild/buildtools/file" |
| "github.com/bazelbuild/buildtools/labels" |
| "github.com/bazelbuild/buildtools/wspace" |
| "github.com/golang/protobuf/jsonpb" |
| "github.com/golang/protobuf/proto" |
| ) |
| |
| // Options represents choices about how buildozer should behave. |
| type Options struct { |
| Stdout bool // write changed BUILD file to stdout |
| Buildifier string // path to buildifier binary |
| Parallelism int // number of cores to use for concurrent actions |
| NumIO int // number of concurrent actions |
| CommandsFiles []string // file names to read commands from, use '-' for stdin (format:|-separated command line arguments to buildozer, excluding flags |
| KeepGoing bool // apply all commands, even if there are failures |
| FilterRuleTypes []string // list of rule types to change, empty means all |
| PreferEOLComments bool // when adding a new comment, put it on the same line if possible |
| RootDir string // If present, use this folder rather than $PWD to find the root dir |
| Quiet bool // suppress informational messages. |
| EditVariables bool // for attributes that simply assign a variable (e.g. hdrs = LIB_HDRS), edit the build variable instead of appending to the attribute. |
| IsPrintingProto bool // output serialized devtools.buildozer.Output protos instead of human-readable strings |
| IsPrintingJSON bool // output serialized devtools.buildozer.Output json instead of human-readable strings |
| OutWriter io.Writer // where to write normal output (`os.Stdout` will be used if not specified) |
| ErrWriter io.Writer // where to write error output (`os.Stderr` will be used if not specified) |
| } |
| |
| // NewOpts returns a new Options struct with some defaults set. |
| func NewOpts() *Options { |
| return &Options{NumIO: 200, PreferEOLComments: true} |
| } |
| |
| // Usage is a user-overridden func to print the program usage. |
| var Usage = func() {} |
| |
| const stdinPackageName = "-" // the special package name to represent stdin |
| |
| // CmdEnvironment stores the information the commands below have access to. |
| type CmdEnvironment struct { |
| File *build.File // the AST |
| Rule *build.Rule // the rule to modify |
| Vars map[string]*build.AssignExpr // global variables set in the build file |
| Pkg string // the full package name |
| Args []string // the command-line arguments |
| output *apipb.Output_Record // output proto, stores whatever a command wants to print |
| } |
| |
| // The cmdXXX functions implement the various commands. |
| |
| func cmdAdd(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| for _, val := range env.Args[1:] { |
| if IsIntList(attr) { |
| AddValueToListAttribute(env.Rule, attr, env.Pkg, &build.LiteralExpr{Token: val}, &env.Vars) |
| continue |
| } |
| strVal := getStringExpr(val, env.Pkg) |
| AddValueToListAttribute(env.Rule, attr, env.Pkg, strVal, &env.Vars) |
| } |
| ResolveAttr(env.Rule, attr, env.Pkg) |
| return env.File, nil |
| } |
| |
| func cmdComment(opts *Options, env CmdEnvironment) (*build.File, error) { |
| // The comment string is always the last argument in the list. |
| str := env.Args[len(env.Args)-1] |
| str = strings.Replace(str, "\\n", "\n", -1) |
| // Multiline comments should go on a separate line. |
| fullLine := !opts.PreferEOLComments || strings.Contains(str, "\n") |
| comment := []build.Comment{} |
| for _, line := range strings.Split(str, "\n") { |
| comment = append(comment, build.Comment{Token: "# " + line}) |
| } |
| |
| // The comment might be attached to a rule, an attribute, or a value in a list, |
| // depending on how many arguments are passed. |
| switch len(env.Args) { |
| case 1: // Attach to a rule |
| env.Rule.Call.Comments.Before = comment |
| case 2: // Attach to an attribute |
| if attr := env.Rule.AttrDefn(env.Args[0]); attr != nil { |
| if fullLine { |
| attr.LHS.Comment().Before = comment |
| } else { |
| attr.RHS.Comment().Suffix = comment |
| } |
| } |
| case 3: // Attach to a specific value in a list |
| if attr := env.Rule.Attr(env.Args[0]); attr != nil { |
| if expr := listOrSelectFind(attr, env.Args[1], env.Pkg); expr != nil { |
| if fullLine { |
| expr.Comments.Before = comment |
| } else { |
| expr.Comments.Suffix = comment |
| } |
| } |
| } |
| default: |
| panic("cmdComment") |
| } |
| return env.File, nil |
| } |
| |
| // commentsText concatenates comments into a single line. |
| func commentsText(comments []build.Comment) string { |
| var segments []string |
| for _, comment := range comments { |
| token := comment.Token |
| if strings.HasPrefix(token, "#") { |
| token = token[1:] |
| } |
| segments = append(segments, strings.TrimSpace(token)) |
| } |
| return strings.Replace(strings.Join(segments, " "), "\n", " ", -1) |
| } |
| |
| func cmdPrintComment(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attrError := func() error { |
| return fmt.Errorf("rule \"//%s:%s\" has no attribute \"%s\"", env.Pkg, env.Rule.Name(), env.Args[0]) |
| } |
| |
| switch len(env.Args) { |
| case 0: // Print rule comment. |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Text{Text: commentsText(env.Rule.Call.Comments.Before)}}, |
| } |
| if text := commentsText(env.Rule.Call.Comments.Suffix); text != "" { |
| env.output.Fields = append(env.output.Fields, &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: text}, |
| }) |
| } |
| if text := commentsText(env.Rule.Call.Comments.After); text != "" { |
| env.output.Fields = append(env.output.Fields, &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: text}, |
| }) |
| } |
| case 1: // Print attribute comment. |
| attr := env.Rule.AttrDefn(env.Args[0]) |
| if attr == nil { |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING}}, |
| } |
| return nil, attrError() |
| } |
| comments := append(attr.Before, attr.Suffix...) |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Text{Text: commentsText(comments)}}, |
| } |
| case 2: // Print comment of a specific value in a list. |
| attr := env.Rule.Attr(env.Args[0]) |
| if attr == nil { |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING}}, |
| } |
| return nil, attrError() |
| } |
| value := env.Args[1] |
| expr := listOrSelectFind(attr, value, env.Pkg) |
| if expr == nil { |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING_LIST_ITEM}}, |
| } |
| return nil, fmt.Errorf("attribute \"%s\" has no value \"%s\"", env.Args[0], value) |
| } |
| comments := append(expr.Comments.Before, expr.Comments.Suffix...) |
| env.output.Fields = []*apipb.Output_Record_Field{ |
| {Value: &apipb.Output_Record_Field_Text{Text: commentsText(comments)}}, |
| } |
| default: |
| panic("cmdPrintComment") |
| } |
| return nil, nil |
| } |
| |
| func cmdDelete(opts *Options, env CmdEnvironment) (*build.File, error) { |
| return DeleteRule(env.File, env.Rule), nil |
| } |
| |
| func cmdMove(opts *Options, env CmdEnvironment) (*build.File, error) { |
| oldAttr := env.Args[0] |
| newAttr := env.Args[1] |
| if len(env.Args) == 3 && env.Args[2] == "*" { |
| if err := MoveAllListAttributeValues(env.Rule, oldAttr, newAttr, env.Pkg, &env.Vars); err != nil { |
| return nil, err |
| } |
| return env.File, nil |
| } |
| fixed := false |
| for _, val := range env.Args[2:] { |
| if deleted := ListAttributeDelete(env.Rule, oldAttr, val, env.Pkg); deleted != nil { |
| AddValueToListAttribute(env.Rule, newAttr, env.Pkg, deleted, &env.Vars) |
| fixed = true |
| } |
| } |
| if fixed { |
| return env.File, nil |
| } |
| return nil, nil |
| } |
| |
| func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { |
| kind := env.Args[0] |
| name := env.Args[1] |
| addAtEOF, insertionIndex, err := findInsertionIndex(env) |
| if err != nil { |
| return nil, err |
| } |
| |
| if FindRuleByName(env.File, name) != nil { |
| return nil, fmt.Errorf("rule '%s' already exists", name) |
| } |
| |
| call := &build.CallExpr{X: &build.Ident{Name: kind}} |
| rule := &build.Rule{Call: call, ImplicitName: ""} |
| rule.SetAttr("name", &build.StringExpr{Value: name}) |
| |
| if addAtEOF { |
| env.File.Stmt = InsertAfterLastOfSameKind(env.File.Stmt, rule.Call) |
| } else { |
| env.File.Stmt = InsertAfter(insertionIndex, env.File.Stmt, call) |
| } |
| return env.File, nil |
| } |
| |
| // findInsertionIndex is used by cmdNew to find the place at which to insert the new rule. |
| func findInsertionIndex(env CmdEnvironment) (bool, int, error) { |
| if len(env.Args) < 4 { |
| return true, 0, nil |
| } |
| |
| relativeToRuleName := env.Args[3] |
| ruleIdx, _ := IndexOfRuleByName(env.File, relativeToRuleName) |
| if ruleIdx == -1 { |
| return true, 0, nil |
| } |
| |
| switch env.Args[2] { |
| case "before": |
| return false, ruleIdx - 1, nil |
| case "after": |
| return false, ruleIdx, nil |
| default: |
| return true, 0, fmt.Errorf("Unknown relative operator '%s'; allowed: 'before', 'after'", env.Args[1]) |
| } |
| } |
| |
| // splitLoadArgs splits arguments of form <[to=]from> |
| // into a slice of froms and a slice of tos. |
| func splitLoadArgs(args []string) ([]string, []string) { |
| from := args |
| to := append([]string{}, args...) |
| for i := range from { |
| if s := strings.SplitN(from[i], "=", 2); len(s) == 2 { |
| to[i] = s[0] |
| from[i] = s[1] |
| } |
| } |
| |
| return from, to |
| } |
| |
| func cmdNewLoad(opts *Options, env CmdEnvironment) (*build.File, error) { |
| from, to := splitLoadArgs(env.Args[1:]) |
| env.File.Stmt = InsertLoad(env.File.Stmt, env.Args[0], from, to) |
| return env.File, nil |
| } |
| |
| func cmdReplaceLoad(opts *Options, env CmdEnvironment) (*build.File, error) { |
| from, to := splitLoadArgs(env.Args[1:]) |
| env.File.Stmt = ReplaceLoad(env.File.Stmt, env.Args[0], from, to) |
| return env.File, nil |
| } |
| |
| func cmdSubstituteLoad(opts *Options, env CmdEnvironment) (*build.File, error) { |
| oldRegexp, err := regexp.Compile(env.Args[0]) |
| if err != nil { |
| return nil, err |
| } |
| newTemplate := env.Args[1] |
| |
| for _, stmt := range env.File.Stmt { |
| load, ok := stmt.(*build.LoadStmt) |
| if !ok { |
| continue |
| } |
| |
| if newValue, ok := stringSubstitute(load.Module.Value, oldRegexp, newTemplate); ok { |
| load.Module.Value = newValue |
| } |
| } |
| |
| return env.File, nil |
| } |
| |
| func cmdPrint(opts *Options, env CmdEnvironment) (*build.File, error) { |
| format := env.Args |
| if len(format) == 0 { |
| format = []string{"name", "kind"} |
| } |
| fields := make([]*apipb.Output_Record_Field, len(format)) |
| |
| for i, str := range format { |
| value := env.Rule.Attr(str) |
| if str == "kind" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: env.Rule.Kind()}, |
| } |
| } else if str == "name" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: env.Rule.Name()}, |
| } |
| } else if str == "label" { |
| if env.Rule.Name() != "" { |
| label := labels.Label{Package: env.Pkg, Target: env.Rule.Name()} |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: label.Format()}, |
| } |
| } else { |
| return nil, nil |
| } |
| } else if str == "rule" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: build.FormatString(env.Rule.Call)}, |
| } |
| } else if str == "startline" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Number{Number: int32(env.Rule.Call.ListStart.Line)}, |
| } |
| } else if str == "endline" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Number{Number: int32(env.Rule.Call.End.Pos.Line)}, |
| } |
| } else if str == "path" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: env.File.Path}, |
| } |
| } else if str == "attrs" { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_List{List: &apipb.RepeatedString{Strings: env.Rule.AttrKeys()}}, |
| } |
| } else if value == nil { |
| fmt.Fprintf(opts.ErrWriter, "rule \"//%s:%s\" has no attribute \"%s\"\n", |
| env.Pkg, env.Rule.Name(), str) |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING}, |
| } |
| } else if lit, ok := value.(*build.LiteralExpr); ok { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: lit.Token}, |
| } |
| } else if lit, ok := value.(*build.Ident); ok { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: lit.Name}, |
| } |
| } else if string, ok := value.(*build.StringExpr); ok { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: string.Value}, |
| QuoteWhenPrinting: true, |
| } |
| } else if strList := env.Rule.AttrStrings(str); strList != nil { |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_List{List: &apipb.RepeatedString{Strings: strList}}, |
| } |
| } else { |
| // Some other Expr we haven't listed above. Just print it. |
| fields[i] = &apipb.Output_Record_Field{ |
| Value: &apipb.Output_Record_Field_Text{Text: build.FormatString(value)}, |
| } |
| } |
| } |
| |
| env.output.Fields = fields |
| return nil, nil |
| } |
| |
| func attrKeysForPattern(rule *build.Rule, pattern string) []string { |
| if pattern == "*" { |
| return rule.AttrKeys() |
| } |
| return []string{pattern} |
| } |
| |
| func cmdRemove(opts *Options, env CmdEnvironment) (*build.File, error) { |
| if len(env.Args) == 1 { // Remove the attribute |
| if env.Args[0] == "*" { |
| didDelete := false |
| for _, attr := range env.Rule.AttrKeys() { |
| if attr == "name" { |
| continue |
| } |
| if env.Rule.DelAttr(attr) != nil { |
| didDelete = true |
| } |
| } |
| if didDelete { |
| return env.File, nil |
| } |
| } else { |
| if env.Rule.DelAttr(env.Args[0]) != nil { |
| return env.File, nil |
| } |
| } |
| } else { // Remove values in the attribute. |
| fixed := false |
| for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { |
| for _, val := range env.Args[1:] { |
| ListAttributeDelete(env.Rule, key, val, env.Pkg) |
| fixed = true |
| } |
| ResolveAttr(env.Rule, key, env.Pkg) |
| // Remove the attribute if's an empty list |
| if listExpr, ok := env.Rule.Attr(key).(*build.ListExpr); ok && len(listExpr.List) == 0 { |
| env.Rule.DelAttr(key) |
| } |
| } |
| if fixed { |
| return env.File, nil |
| } |
| } |
| return nil, nil |
| } |
| |
| func cmdRemoveIfEqual(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| val := env.Args[1] |
| |
| var equal bool |
| switch input := env.Rule.Attr(attr).(type) { |
| case *build.StringExpr: |
| equal = labels.Equal(input.Value, val, env.Pkg) |
| case *build.Ident: |
| equal = input.Name == val |
| default: |
| return nil, nil |
| } |
| |
| if !equal { |
| return nil, nil |
| } |
| |
| env.Rule.DelAttr(attr) |
| return env.File, nil |
| } |
| |
| func cmdRemoveComment(opts *Options, env CmdEnvironment) (*build.File, error) { |
| switch len(env.Args) { |
| case 0: // Remove comment attached to rule |
| env.Rule.Call.Comments.Before = nil |
| env.Rule.Call.Comments.Suffix = nil |
| env.Rule.Call.Comments.After = nil |
| case 1: // Remove comment attached to attr |
| if attr := env.Rule.AttrDefn(env.Args[0]); attr != nil { |
| attr.Comments.Before = nil |
| attr.Comments.Suffix = nil |
| attr.Comments.After = nil |
| attr.LHS.Comment().Before = nil |
| attr.LHS.Comment().Suffix = nil |
| attr.LHS.Comment().After = nil |
| attr.RHS.Comment().Before = nil |
| attr.RHS.Comment().Suffix = nil |
| attr.RHS.Comment().After = nil |
| } |
| case 2: // Remove comment attached to value |
| if attr := env.Rule.Attr(env.Args[0]); attr != nil { |
| if expr := listOrSelectFind(attr, env.Args[1], env.Pkg); expr != nil { |
| expr.Comments.Before = nil |
| expr.Comments.Suffix = nil |
| expr.Comments.After = nil |
| } |
| } |
| default: |
| panic("cmdRemoveComment") |
| } |
| return env.File, nil |
| } |
| |
| func cmdRename(opts *Options, env CmdEnvironment) (*build.File, error) { |
| oldAttr := env.Args[0] |
| newAttr := env.Args[1] |
| if err := RenameAttribute(env.Rule, oldAttr, newAttr); err != nil { |
| return nil, err |
| } |
| return env.File, nil |
| } |
| |
| func cmdReplace(opts *Options, env CmdEnvironment) (*build.File, error) { |
| oldV := getStringValue(env.Args[1]) |
| newV := getStringValue(env.Args[2]) |
| for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { |
| attr := env.Rule.Attr(key) |
| if e, ok := attr.(*build.StringExpr); ok { |
| if labels.Equal(e.Value, oldV, env.Pkg) { |
| env.Rule.SetAttr(key, getAttrValueExpr(key, []string{newV}, env)) |
| } |
| } else { |
| ListReplace(attr, oldV, newV, env.Pkg) |
| } |
| } |
| return env.File, nil |
| } |
| |
| func cmdSubstitute(opts *Options, env CmdEnvironment) (*build.File, error) { |
| oldRegexp, err := regexp.Compile(env.Args[1]) |
| if err != nil { |
| return nil, err |
| } |
| newTemplate := env.Args[2] |
| for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { |
| attr := env.Rule.Attr(key) |
| e, ok := attr.(*build.StringExpr) |
| if !ok { |
| ListSubstitute(attr, oldRegexp, newTemplate) |
| continue |
| } |
| if newValue, ok := stringSubstitute(e.Value, oldRegexp, newTemplate); ok { |
| env.Rule.SetAttr(key, getStringExpr(newValue, env.Pkg)) |
| } |
| } |
| return env.File, nil |
| } |
| |
| func cmdSet(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| if attr == "kind" { |
| env.Rule.SetKind(args[0]) |
| } else { |
| env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env)) |
| } |
| return env.File, nil |
| } |
| |
| func cmdSetIfAbsent(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| if attr == "kind" { |
| return nil, fmt.Errorf("setting 'kind' is not allowed for set_if_absent. Got %s", env.Args) |
| } |
| if env.Rule.Attr(attr) == nil { |
| env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env)) |
| } |
| return env.File, nil |
| } |
| |
| func getAttrValueExpr(attr string, args []string, env CmdEnvironment) build.Expr { |
| switch { |
| case attr == "kind": |
| return nil |
| case IsIntList(attr): |
| var list []build.Expr |
| for _, i := range args { |
| list = append(list, &build.LiteralExpr{Token: i}) |
| } |
| return &build.ListExpr{List: list} |
| case IsList(attr) && !(len(args) == 1 && strings.HasPrefix(args[0], "glob(")): |
| var list []build.Expr |
| for _, arg := range args { |
| list = append(list, getStringExpr(arg, env.Pkg)) |
| } |
| return &build.ListExpr{List: list} |
| case len(args) == 0: |
| // Expected a non-list argument, nothing provided |
| return &build.Ident{Name: "None"} |
| case IsString(attr): |
| return getStringExpr(args[0], env.Pkg) |
| default: |
| return &build.Ident{Name: args[0]} |
| } |
| } |
| |
| // getStringValue extracts a string value, which can be either quoted or not, from an input argument |
| func getStringValue(value string) string { |
| if unquoted, _, err := build.Unquote(value); err == nil { |
| return unquoted |
| } |
| return value |
| } |
| |
| // getStringExpr creates a StringExpr from an input argument, which can be either quoted or not, |
| // and shortens the label value if possible. |
| func getStringExpr(value, pkg string) build.Expr { |
| if unquoted, triple, err := build.Unquote(value); err == nil { |
| return &build.StringExpr{Value: ShortenLabel(unquoted, pkg), TripleQuote: triple} |
| } |
| return &build.StringExpr{Value: ShortenLabel(value, pkg)} |
| } |
| |
| func cmdCopy(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attrName := env.Args[0] |
| from := env.Args[1] |
| |
| return copyAttributeBetweenRules(env, attrName, from) |
| } |
| |
| func cmdCopyNoOverwrite(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attrName := env.Args[0] |
| from := env.Args[1] |
| |
| if env.Rule.Attr(attrName) != nil { |
| return env.File, nil |
| } |
| |
| return copyAttributeBetweenRules(env, attrName, from) |
| } |
| |
| // cmdDictAdd adds a key to a dict, if that key does _not_ exit already. |
| func cmdDictAdd(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| |
| dict := &build.DictExpr{} |
| currDict, ok := env.Rule.Attr(attr).(*build.DictExpr) |
| if ok { |
| dict = currDict |
| } |
| |
| for _, x := range args { |
| kv := strings.SplitN(x, ":", 2) |
| if len(kv) != 2 { |
| return nil, fmt.Errorf("no colon in dict_add argument %q found", x) |
| } |
| expr := getStringExpr(kv[1], env.Pkg) |
| |
| prev := DictionaryGet(dict, kv[0]) |
| if prev == nil { |
| // Only set the value if the value is currently unset. |
| DictionarySet(dict, kv[0], expr) |
| } |
| } |
| env.Rule.SetAttr(attr, dict) |
| return env.File, nil |
| } |
| |
| func cmdSetSelect(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| |
| dict := &build.DictExpr{} |
| |
| if len(args)%2 != 0 { |
| return nil, fmt.Errorf("no value passed for last key: %s", args[len(args)-1]) |
| } |
| for i := 0; i < len(args); i += 2 { |
| key := args[i] |
| value := args[i+1] |
| var expr build.Expr |
| if IsList(attr) { |
| list := &build.ListExpr{} |
| if cur := DictionaryGet(dict, key); cur != nil { |
| list = cur.(*build.ListExpr) |
| } |
| AddValueToList(list, env.Pkg, getStringExpr(value, env.Pkg), !attributeMustNotBeSorted(env.Rule.Name(), attr)) |
| expr = list |
| } else { |
| expr = getStringExpr(value, env.Pkg) |
| } |
| // Set overwrites previous values. |
| DictionarySet(dict, key, expr) |
| } |
| call := &build.CallExpr{List: []build.Expr{dict}} |
| call.X = &build.Ident{Name: "select"} |
| env.Rule.SetAttr(attr, call) |
| return env.File, nil |
| } |
| |
| // cmdDictSet adds a key to a dict, overwriting any previous values. |
| func cmdDictSet(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| |
| dict := &build.DictExpr{} |
| currDict, ok := env.Rule.Attr(attr).(*build.DictExpr) |
| if ok { |
| dict = currDict |
| } |
| |
| for _, x := range args { |
| kv := strings.SplitN(x, ":", 2) |
| if len(kv) != 2 { |
| return nil, fmt.Errorf("no colon in dict_set argument %q found", x) |
| } |
| expr := getStringExpr(kv[1], env.Pkg) |
| // Set overwrites previous values. |
| DictionarySet(dict, kv[0], expr) |
| } |
| env.Rule.SetAttr(attr, dict) |
| return env.File, nil |
| } |
| |
| // cmdDictRemove removes a key from a dict. |
| func cmdDictRemove(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| args := env.Args[1:] |
| |
| thing := env.Rule.Attr(attr) |
| dictAttr, ok := thing.(*build.DictExpr) |
| if !ok { |
| return env.File, nil |
| } |
| |
| for _, x := range args { |
| // should errors here be flagged? |
| DictionaryDelete(dictAttr, x) |
| env.Rule.SetAttr(attr, dictAttr) |
| } |
| |
| // If the removal results in the dict having no contents, delete the attribute (stay clean!) |
| if dictAttr == nil || len(dictAttr.List) == 0 { |
| env.Rule.DelAttr(attr) |
| } |
| |
| return env.File, nil |
| } |
| |
| // cmdDictReplaceIfEqual updates a value in a dict if it is equal to a given value. |
| func cmdDictReplaceIfEqual(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| key := env.Args[1] |
| oldV := getStringValue(env.Args[2]) |
| newV := getStringValue(env.Args[3]) |
| |
| thing := env.Rule.Attr(attr) |
| dictAttr, ok := thing.(*build.DictExpr) |
| if !ok { |
| return env.File, nil |
| } |
| |
| prev := DictionaryGet(dictAttr, key) |
| if prev == nil { |
| return nil, fmt.Errorf("key '%s' not found in dict", key) |
| } |
| if e, ok := prev.(*build.StringExpr); ok { |
| if labels.Equal(e.Value, oldV, env.Pkg) { |
| DictionarySet(dictAttr, key, getStringExpr(newV, env.Pkg)) |
| } |
| } else if e, ok := prev.(*build.Ident); ok { |
| if e.Name == oldV { |
| DictionarySet(dictAttr, key, getStringExpr(newV, env.Pkg)) |
| } |
| } |
| return env.File, nil |
| } |
| |
| // cmdDictListAdd adds an item to a list in a dict. |
| func cmdDictListAdd(opts *Options, env CmdEnvironment) (*build.File, error) { |
| attr := env.Args[0] |
| key := env.Args[1] |
| args := env.Args[2:] |
| |
| dict := &build.DictExpr{} |
| if currDict, ok := env.Rule.Attr(attr).(*build.DictExpr); ok { |
| dict = currDict |
| } |
| |
| prev := DictionaryGet(dict, key) |
| if prev == nil { |
| prev = &build.ListExpr{} |
| } |
| |
| for _, val := range args { |
| expr := getStringExpr(val, env.Pkg) |
| prev = AddValueToList(prev, env.Pkg, expr, true) |
| } |
| |
| DictionarySet(dict, key, prev) |
| env.Rule.SetAttr(attr, dict) |
| |
| return env.File, nil |
| } |
| |
| func copyAttributeBetweenRules(env CmdEnvironment, attrName string, from string) (*build.File, error) { |
| fromRule := FindRuleByName(env.File, from) |
| if fromRule == nil { |
| return nil, fmt.Errorf("could not find rule '%s'", from) |
| } |
| attr := fromRule.Attr(attrName) |
| if attr == nil { |
| return nil, fmt.Errorf("rule '%s' does not have attribute '%s'", from, attrName) |
| } |
| |
| ast, err := build.ParseBuild("" /* filename */, []byte(build.FormatString(attr))) |
| if err != nil { |
| return nil, fmt.Errorf("could not parse attribute value %v", build.FormatString(attr)) |
| } |
| |
| env.Rule.SetAttr(attrName, ast.Stmt[0]) |
| return env.File, nil |
| } |
| |
| func cmdUseRepoAdd(opts *Options, env CmdEnvironment) (*build.File, error) { |
| return cmdImplUseRepo(env, "use_repo_add") |
| } |
| |
| func cmdUseRepoRemove(opts *Options, env CmdEnvironment) (*build.File, error) { |
| return cmdImplUseRepo(env, "use_repo_remove") |
| } |
| |
| func cmdImplUseRepo(env CmdEnvironment, mode string) (*build.File, error) { |
| if env.File.Type != build.TypeModule { |
| return nil, fmt.Errorf("%s: only applies to MODULE.bazel files", mode) |
| } |
| |
| dev := false |
| args := env.Args |
| if env.Args[0] == "dev" && isExtensionLabel(env.Args[1]) { |
| dev = true |
| args = env.Args[1:] |
| } |
| |
| var proxies []string |
| var repos []string |
| if isExtensionLabel(args[0]) { |
| extBzlFile := args[0] |
| extName := args[1] |
| |
| proxies = bzlmod.Proxies(env.File, extBzlFile, extName, dev) |
| if len(proxies) == 0 { |
| return nil, fmt.Errorf("%s: no use_extension assignment found for extension %q defined in %q", mode, extName, extBzlFile) |
| } |
| repos = args[2:] |
| } else { |
| proxy := args[0] |
| |
| proxies = bzlmod.AllProxies(env.File, proxy) |
| if len(proxies) == 0 { |
| return nil, fmt.Errorf("%s: no use_extension assignment to variable %q found", mode, proxy) |
| } |
| repos = args[1:] |
| } |
| |
| useRepos := bzlmod.UseRepos(env.File, proxies) |
| if len(useRepos) == 0 { |
| var newUseRepo *build.CallExpr |
| env.File, newUseRepo = bzlmod.NewUseRepo(env.File, proxies) |
| useRepos = []*build.CallExpr{newUseRepo} |
| } |
| |
| if mode == "use_repo_add" { |
| bzlmod.AddRepoUsages(useRepos, repos...) |
| } else { |
| bzlmod.RemoveRepoUsages(useRepos, repos...) |
| } |
| |
| return env.File, nil |
| } |
| |
| func cmdFormat(opts *Options, env CmdEnvironment) (*build.File, error) { |
| // Force formatting by not returning a nil *build.File. |
| return env.File, nil |
| } |
| |
| func isExtensionLabel(arg string) bool { |
| // Labels referencing extensions are either absolute or repo-absolute. Repository names are not |
| // allowed to contain "@" or "/". |
| return strings.HasPrefix(arg, "@") || strings.HasSuffix(arg, "//") |
| } |
| |
| func cmdFix(opts *Options, env CmdEnvironment) (*build.File, error) { |
| // Fix the whole file |
| if env.Rule.Kind() == "package" { |
| return FixFile(env.File, env.Pkg, env.Args), nil |
| } |
| // Fix a specific rule |
| return FixRule(env.File, env.Pkg, env.Rule, env.Args), nil |
| } |
| |
| // CommandInfo provides a command function and info on incoming arguments. |
| type CommandInfo struct { |
| Fn func(*Options, CmdEnvironment) (*build.File, error) |
| PerRule bool |
| MinArg int |
| MaxArg int |
| Template string |
| } |
| |
| // AllCommands associates the command names with their function and number |
| // of arguments. |
| var AllCommands = map[string]CommandInfo{ |
| "add": {cmdAdd, true, 2, -1, "<attr> <value(s)>"}, |
| "new_load": {cmdNewLoad, false, 1, -1, "<path> <[to=]from(s)>"}, |
| "replace_load": {cmdReplaceLoad, false, 1, -1, "<path> <[to=]symbol(s)>"}, |
| "substitute_load": {cmdSubstituteLoad, false, 2, 2, "<old_regexp> <new_template>"}, |
| "comment": {cmdComment, true, 1, 3, "<attr>? <value>? <comment>"}, |
| "print_comment": {cmdPrintComment, true, 0, 2, "<attr>? <value>?"}, |
| "delete": {cmdDelete, true, 0, 0, ""}, |
| "fix": {cmdFix, true, 0, -1, "<fix(es)>?"}, |
| "move": {cmdMove, true, 3, -1, "<old_attr> <new_attr> <value(s)>"}, |
| "new": {cmdNew, false, 2, 4, "<rule_kind> <rule_name> [(before|after) <relative_rule_name>]"}, |
| "print": {cmdPrint, true, 0, -1, "<attribute(s)>"}, |
| "remove": {cmdRemove, true, 1, -1, "<attr> <value(s)>"}, |
| "remove_comment": {cmdRemoveComment, true, 0, 2, "<attr>? <value>?"}, |
| "remove_if_equal": {cmdRemoveIfEqual, true, 2, 2, "<attr> <value>"}, |
| "rename": {cmdRename, true, 2, 2, "<old_attr> <new_attr>"}, |
| "replace": {cmdReplace, true, 3, 3, "<attr> <old_value> <new_value>"}, |
| "substitute": {cmdSubstitute, true, 3, 3, "<attr> <old_regexp> <new_template>"}, |
| "set": {cmdSet, true, 1, -1, "<attr> <value(s)>"}, |
| "set_if_absent": {cmdSetIfAbsent, true, 1, -1, "<attr> <value(s)>"}, |
| "set_select": {cmdSetSelect, true, 1, -1, "<attr> <key_1> <value_1> <key_n> <value_n>"}, |
| "copy": {cmdCopy, true, 2, 2, "<attr> <from_rule>"}, |
| "copy_no_overwrite": {cmdCopyNoOverwrite, true, 2, 2, "<attr> <from_rule>"}, |
| "dict_add": {cmdDictAdd, true, 2, -1, "<attr> <(key:value)(s)>"}, |
| "dict_set": {cmdDictSet, true, 2, -1, "<attr> <(key:value)(s)>"}, |
| "dict_remove": {cmdDictRemove, true, 2, -1, "<attr> <key(s)>"}, |
| "dict_replace_if_equal": {cmdDictReplaceIfEqual, true, 4, 4, "<attr> <key> <old_value> <new_value>"}, |
| "dict_list_add": {cmdDictListAdd, true, 3, -1, "<attr> <key> <value(s)>"}, |
| "use_repo_add": {cmdUseRepoAdd, false, 2, -1, "([dev] <extension .bzl file> <extension name>|<use_extension variable name>) <repo(s)>"}, |
| "use_repo_remove": {cmdUseRepoRemove, false, 2, -1, "([dev] <extension .bzl file> <extension name>|<use_extension variable name>) <repo(s)>"}, |
| "format": {cmdFormat, false, 0, 0, ""}, |
| } |
| |
| var readonlyCommands = map[string]bool{ |
| "print": true, |
| "print_comment": true, |
| } |
| |
| func expandTargets(f *build.File, rule string) ([]*build.Rule, error) { |
| if r := FindRuleByName(f, rule); r != nil { |
| return []*build.Rule{r}, nil |
| } else if r := FindExportedFile(f, rule); r != nil { |
| return []*build.Rule{r}, nil |
| } else if rule == "all" || rule == "*" { |
| // "all" is a valid name, it is a wildcard only if no such rule is found. |
| return f.Rules(""), nil |
| } else if strings.HasPrefix(rule, "%") { |
| // "%java_library" will match all java_library functions in the package |
| // "%<LINENUM>" will match the rule which begins at LINENUM. |
| // This is for convenience, "%" is not a valid character in bazel targets. |
| kind := rule[1:] |
| if linenum, err := strconv.Atoi(kind); err == nil { |
| if r := f.RuleAt(linenum); r != nil { |
| return []*build.Rule{r}, nil |
| } |
| } else { |
| return f.Rules(kind), nil |
| } |
| } |
| return nil, fmt.Errorf("rule '%s' not found", rule) |
| } |
| |
| func filterRules(opts *Options, rules []*build.Rule) (result []*build.Rule) { |
| if len(opts.FilterRuleTypes) == 0 { |
| return rules |
| } |
| for _, rule := range rules { |
| for _, filterType := range opts.FilterRuleTypes { |
| if rule.Kind() == filterType { |
| result = append(result, rule) |
| break |
| } |
| } |
| } |
| return |
| } |
| |
| // command contains a list of tokens that describe a buildozer command. |
| type command struct { |
| tokens []string |
| } |
| |
| // checkCommandUsage checks the number of argument of a command. |
| // It prints an error and usage when it is not valid. |
| func checkCommandUsage(opts *Options, name string, cmd CommandInfo, count int) { |
| if count >= cmd.MinArg && (cmd.MaxArg == -1 || count <= cmd.MaxArg) { |
| return |
| } |
| |
| if count < cmd.MinArg { |
| fmt.Fprintf(opts.ErrWriter, "Too few arguments for command '%s', expected at least %d.\n", |
| name, cmd.MinArg) |
| } else { |
| fmt.Fprintf(opts.ErrWriter, "Too many arguments for command '%s', expected at most %d.\n", |
| name, cmd.MaxArg) |
| } |
| Usage() |
| os.Exit(1) |
| } |
| |
| // Match text that only contains spaces or line breaks if they're escaped with '\'. |
| var spaceRegex = regexp.MustCompile(`(\\ |\\\n|[^ \n])+`) |
| |
| // SplitOnSpaces behaves like strings.Fields, except that spaces can be escaped. |
| // Also splits on linebreaks unless they are escaped too. |
| // " some dummy\\ string" -> ["some", "dummy string"] |
| func SplitOnSpaces(input string) []string { |
| result := spaceRegex.FindAllString(input, -1) |
| for i, s := range result { |
| s = strings.Replace(s, `\ `, " ", -1) |
| s = strings.Replace(s, "\\\n", "\n", -1) |
| result[i] = s |
| } |
| return result |
| } |
| |
| // parseCommands parses commands and targets they should be applied on from |
| // a list of arguments. |
| // Each argument can be either: |
| // - a command (as defined by AllCommands) and its parameters, separated by |
| // whitespace |
| // - a target all commands that are parsed during one call to parseCommands |
| // should be applied on |
| func parseCommands(opts *Options, args []string) (commands []command, targets []string, err error) { |
| for _, arg := range args { |
| commandTokens := SplitOnSpaces(arg) |
| if len(commandTokens) == 0 { |
| return nil, nil, fmt.Errorf("empty command list") |
| } |
| |
| cmd, found := AllCommands[commandTokens[0]] |
| if found { |
| checkCommandUsage(opts, commandTokens[0], cmd, len(commandTokens)-1) |
| commands = append(commands, command{commandTokens}) |
| } else { |
| targets = append(targets, arg) |
| } |
| } |
| return |
| } |
| |
| // commandsForTarget contains commands to be executed on the given target. |
| type commandsForTarget struct { |
| target string |
| commands []command |
| } |
| |
| // commandsForFile contains the file name and all commands that should be |
| // applied on that file, indexed by their target. |
| type commandsForFile struct { |
| file string |
| commands []commandsForTarget |
| } |
| |
| // commandError returns an error that formats 'err' in the context of the |
| // commands to be executed on the given target. |
| func commandError(commands []command, target string, err error) error { |
| return fmt.Errorf("error while executing commands %s on target %s: %s", commands, target, err) |
| } |
| |
| // rewriteResult contains the outcome of applying fixes to a single file. |
| type rewriteResult struct { |
| file string |
| errs []error |
| modified bool |
| records []*apipb.Output_Record |
| } |
| |
| // getGlobalVariables returns the global variable assignments in the provided list of expressions. |
| // That is, for each variable assignment of the form |
| // |
| // a = v |
| // |
| // vars["a"] will contain the AssignExpr whose RHS value is the assignment "a = v". |
| 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 |
| } |
| } |
| } |
| return vars |
| } |
| |
| // When checking the filesystem, we need to look for any of the |
| // possible BuildFileNames. For historical reasons, the |
| // parts of the tool that generate paths that we may want to examine |
| // continue to assume that build files are all named "BUILD". |
| |
| // BuildFileNames is exported so that users that want to override it |
| // in scripts are free to do so. |
| var BuildFileNames = [...]string{"BUILD.bazel", "BUILD", "BUCK"} |
| |
| // Buildifier formats the build file using the buildifier logic. |
| type Buildifier interface { |
| // Buildify formats the build file and returns the formatted contents. |
| Buildify(*Options, *build.File) ([]byte, error) |
| } |
| |
| var ( |
| buildifier Buildifier = &defaultBuildifier{} |
| buildifierRegistered = false |
| ) |
| |
| // RegisterBuildifier replaces the default buildifier with an |
| // alternative implementation. |
| // |
| // It may only be called once. |
| func RegisterBuildifier(b Buildifier) { |
| if buildifierRegistered { |
| panic("Only one call to RegisterBuildifier is allowed.") |
| } |
| buildifier = b |
| buildifierRegistered = true |
| } |
| |
| // rewrite parses the BUILD file for the given file, transforms the AST, |
| // and write the changes back in the file (or on stdout). |
| func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult { |
| name := commandsForFile.file |
| var data []byte |
| var err error |
| var fi os.FileInfo |
| records := []*apipb.Output_Record{} |
| if name == stdinPackageName { // read on stdin |
| data, err = io.ReadAll(os.Stdin) |
| if err != nil { |
| return &rewriteResult{file: name, errs: []error{err}} |
| } |
| } else { |
| origName := name |
| for _, suffix := range BuildFileNames { |
| if strings.HasSuffix(name, "/"+suffix) { |
| name = strings.TrimSuffix(name, suffix) |
| break |
| } |
| } |
| for _, suffix := range BuildFileNames { |
| name = name + suffix |
| data, fi, err = file.ReadFile(name) |
| if err == nil { |
| break |
| } |
| name = strings.TrimSuffix(name, suffix) |
| } |
| if err != nil { |
| data, fi, err = file.ReadFile(name) |
| } |
| if err != nil { |
| err = errors.New("file not found or not readable") |
| return &rewriteResult{file: origName, errs: []error{err}} |
| } |
| } |
| |
| f, err := build.Parse(name, data) |
| if err != nil { |
| return &rewriteResult{file: name, errs: []error{err}} |
| } |
| if f.Type == build.TypeDefault { |
| // Buildozer is unable to infer the file type, fall back to BUILD by default. |
| f.Type = build.TypeBuild |
| } |
| f.WorkspaceRoot, f.Pkg, f.Label = wspace.SplitFilePath(name) |
| |
| vars := map[string]*build.AssignExpr{} |
| if opts.EditVariables { |
| vars = getGlobalVariables(f.Stmt) |
| } |
| var errs []error |
| changed := false |
| for _, commands := range commandsForFile.commands { |
| target := commands.target |
| commands := commands.commands |
| _, _, absPkg, rule := InterpretLabelForWorkspaceLocation(opts.RootDir, target) |
| if label := labels.Parse(target); label.Package == stdinPackageName { |
| // Special-case: This is already absolute |
| absPkg = stdinPackageName |
| } |
| if strings.HasSuffix(absPkg, "...") { |
| // Special case: the provided target contains an ellipsis, use the file package |
| absPkg = f.Pkg |
| } |
| |
| targets, err := expandTargets(f, rule) |
| if err != nil { |
| cerr := commandError(commands, target, err) |
| errs = append(errs, cerr) |
| if !opts.KeepGoing { |
| return &rewriteResult{file: name, errs: errs, records: records} |
| } |
| } |
| targets = filterRules(opts, targets) |
| for _, cmd := range commands { |
| 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. |
| cmdTargets := targets |
| if !cmdInfo.PerRule { |
| cmdTargets = []*build.Rule{nil} |
| } |
| for _, r := range cmdTargets { |
| record := &apipb.Output_Record{} |
| newf, err := cmdInfo.Fn(opts, CmdEnvironment{f, r, vars, absPkg, cmd.tokens[1:], record}) |
| if len(record.Fields) != 0 { |
| records = append(records, record) |
| } |
| if err != nil { |
| cerr := commandError([]command{cmd}, target, err) |
| if opts.KeepGoing { |
| errs = append(errs, cerr) |
| } else { |
| return &rewriteResult{file: name, errs: []error{cerr}, records: records} |
| } |
| } |
| if newf != nil { |
| changed = true |
| f = newf |
| } |
| } |
| } |
| } |
| if !changed { |
| return &rewriteResult{file: name, errs: errs, records: records} |
| } |
| f = RemoveEmptyPackage(f) |
| f = RemoveEmptyUseRepoCalls(f) |
| ndata, err := buildifier.Buildify(opts, f) |
| if err != nil { |
| return &rewriteResult{file: name, errs: []error{fmt.Errorf("running buildifier: %v", err)}, records: records} |
| } |
| |
| if opts.Stdout || name == stdinPackageName { |
| opts.OutWriter.Write(ndata) |
| return &rewriteResult{file: name, errs: errs, records: records} |
| } |
| |
| if bytes.Equal(data, ndata) { |
| return &rewriteResult{file: name, errs: errs, records: records} |
| } |
| |
| if err := EditFile(fi, name); err != nil { |
| return &rewriteResult{file: name, errs: []error{err}, records: records} |
| } |
| |
| if err := file.WriteFile(name, ndata); err != nil { |
| return &rewriteResult{file: name, errs: []error{err}, records: records} |
| } |
| |
| return &rewriteResult{file: name, errs: errs, modified: true, records: records} |
| } |
| |
| // EditFile is a function that does any prework needed before editing a file. |
| // e.g. "checking out for write" from a locking source control repo. |
| var EditFile = func(fi os.FileInfo, name string) error { |
| return nil |
| } |
| |
| // Given a target, whose package may contain a trailing "/...", returns all |
| // existing BUILD file paths which match the package. |
| func targetExpressionToBuildFiles(rootDir string, target string) []string { |
| file, _, _, _ := InterpretLabelForWorkspaceLocation(rootDir, target) |
| if rootDir == "" { |
| var err error |
| if file, err = filepath.Abs(file); err != nil { |
| fmt.Printf("Cannot make path absolute: %s\n", err.Error()) |
| os.Exit(1) |
| } |
| } |
| |
| suffix := filepath.Join("", "...", "BUILD") // /.../BUILD |
| if !strings.HasSuffix(file, suffix) { |
| return []string{file} |
| } |
| |
| return findBuildFiles(strings.TrimSuffix(file, suffix)) |
| } |
| |
| // Given a root directory, returns all "BUILD" files in that subtree recursively. |
| func findBuildFiles(rootDir string) []string { |
| var buildFiles []string |
| searchDirs := []string{rootDir} |
| |
| for len(searchDirs) != 0 { |
| lastIndex := len(searchDirs) - 1 |
| dir := searchDirs[lastIndex] |
| searchDirs = searchDirs[:lastIndex] |
| |
| dirFiles, err := os.ReadDir(dir) |
| if err != nil { |
| continue |
| } |
| |
| for _, dirFile := range dirFiles { |
| if dirFile.IsDir() { |
| searchDirs = append(searchDirs, filepath.Join(dir, dirFile.Name())) |
| } else { |
| for _, buildFileName := range BuildFileNames { |
| if dirFile.Name() == buildFileName { |
| buildFiles = append(buildFiles, filepath.Join(dir, dirFile.Name())) |
| } |
| } |
| } |
| } |
| } |
| |
| return buildFiles |
| } |
| |
| // appendCommands adds the given commands to be applied to each of the given targets |
| // via the commandMap. |
| func appendCommands(opts *Options, commandMap map[string][]commandsForTarget, args []string) error { |
| commands, targets, err := parseCommands(opts, args) |
| if err != nil { |
| return err |
| } |
| for _, target := range targets { |
| for _, buildFileName := range BuildFileNames { |
| if strings.HasSuffix(target, filepath.FromSlash("/"+buildFileName)) { |
| target = strings.TrimSuffix(target, filepath.FromSlash("/"+buildFileName)) + ":__pkg__" |
| } else if strings.HasSuffix(target, "/"+buildFileName) { |
| target = strings.TrimSuffix(target, "/"+buildFileName) + ":__pkg__" |
| } |
| } |
| var buildFiles []string |
| if label := labels.Parse(target); label.Package == stdinPackageName { |
| buildFiles = []string{stdinPackageName} |
| } else { |
| buildFiles = targetExpressionToBuildFiles(opts.RootDir, target) |
| } |
| |
| for _, file := range buildFiles { |
| commandMap[file] = append(commandMap[file], commandsForTarget{target, commands}) |
| } |
| } |
| return nil |
| } |
| |
| func appendCommandsFromFiles(opts *Options, commandsByFile map[string][]commandsForTarget, labels []string) error { |
| for _, fileName := range opts.CommandsFiles { |
| var reader io.Reader |
| if fileName == stdinPackageName { |
| reader = os.Stdin |
| } else { |
| rc := file.OpenReadFile(fileName) |
| reader = rc |
| defer rc.Close() |
| } |
| if err := appendCommandsFromReader(opts, reader, commandsByFile, labels); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func appendCommandsFromReader(opts *Options, reader io.Reader, commandsByFile map[string][]commandsForTarget, labels []string) error { |
| r := bufio.NewReader(reader) |
| atEOF := false |
| for !atEOF { |
| line, err := r.ReadString('\n') |
| if err == io.EOF { |
| atEOF = true |
| err = nil |
| } |
| if err != nil { |
| return fmt.Errorf("error while reading commands file: %v", err) |
| } |
| line = strings.TrimSpace(line) |
| if line == "" || line[0] == '#' { |
| continue |
| } |
| line = saveEscapedPipes(line) |
| args := strings.Split(line, "|") |
| for i, arg := range args { |
| args[i] = replaceSavedPipes(arg) |
| } |
| if len(args) > 1 && args[1] == "*" { |
| cmd := append([]string{args[0]}, labels...) |
| if err := appendCommands(opts, commandsByFile, cmd); err != nil { |
| return err |
| } |
| } else { |
| if err := appendCommands(opts, commandsByFile, args); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func saveEscapedPipes(s string) string { |
| return strings.ReplaceAll(s, `\|`, "\x00\x00") |
| } |
| |
| func replaceSavedPipes(s string) string { |
| return strings.ReplaceAll(s, "\x00\x00", "|") |
| } |
| |
| func printRecord(writer io.Writer, record *apipb.Output_Record) { |
| fields := record.Fields |
| line := make([]string, len(fields)) |
| for i, field := range fields { |
| switch value := field.Value.(type) { |
| case *apipb.Output_Record_Field_Text: |
| if field.QuoteWhenPrinting && strings.ContainsRune(value.Text, ' ') { |
| line[i] = fmt.Sprintf("%q", value.Text) |
| } else { |
| line[i] = value.Text |
| } |
| case *apipb.Output_Record_Field_Number: |
| line[i] = strconv.Itoa(int(value.Number)) |
| case *apipb.Output_Record_Field_Error: |
| switch value.Error { |
| case apipb.Output_Record_Field_UNKNOWN: |
| line[i] = "(unknown)" |
| case apipb.Output_Record_Field_MISSING: |
| line[i] = "(missing)" |
| } |
| case *apipb.Output_Record_Field_List: |
| line[i] = fmt.Sprintf("[%s]", strings.Join(value.List.Strings, " ")) |
| } |
| } |
| |
| fmt.Fprint(writer, strings.Join(line, " ")+"\n") |
| } |
| |
| // Buildozer loops over all arguments on the command line fixing BUILD files. |
| func Buildozer(opts *Options, args []string) int { |
| if opts.OutWriter == nil { |
| opts.OutWriter = os.Stdout |
| } |
| if opts.ErrWriter == nil { |
| opts.ErrWriter = os.Stderr |
| } |
| commandsByFile := make(map[string][]commandsForTarget) |
| if len(opts.CommandsFiles) > 0 { |
| if err := appendCommandsFromFiles(opts, commandsByFile, args); err != nil { |
| fmt.Fprintf(opts.ErrWriter, "error: %s\n", err) |
| return 1 |
| } |
| } else { |
| if len(args) == 0 { |
| Usage() |
| } |
| if err := appendCommands(opts, commandsByFile, args); err != nil { |
| fmt.Fprintf(opts.ErrWriter, "error: %s\n", err) |
| return 1 |
| } |
| } |
| |
| numFiles := len(commandsByFile) |
| if opts.Parallelism > 0 { |
| runtime.GOMAXPROCS(opts.Parallelism) |
| } |
| results := make(chan *rewriteResult, numFiles) |
| data := make(chan commandsForFile) |
| |
| if opts.NumIO < 1 { |
| fmt.Fprintf(opts.ErrWriter, "NumIO must be at least 1; got %d (are you using `NewOpts`?)\n", opts.NumIO) |
| return 1 |
| } |
| for i := 0; i < opts.NumIO; i++ { |
| go func(results chan *rewriteResult, data chan commandsForFile) { |
| for commandsForFile := range data { |
| results <- rewrite(opts, commandsForFile) |
| } |
| }(results, data) |
| } |
| |
| for file, commands := range commandsByFile { |
| data <- commandsForFile{file, commands} |
| } |
| close(data) |
| records := []*apipb.Output_Record{} |
| var hasErrors bool |
| var fileModified bool |
| for i := 0; i < numFiles; i++ { |
| fileResults := <-results |
| if fileResults == nil { |
| continue |
| } |
| hasErrors = hasErrors || len(fileResults.errs) > 0 |
| fileModified = fileModified || fileResults.modified |
| for _, err := range fileResults.errs { |
| fmt.Fprintf(opts.ErrWriter, "%s: %s\n", fileResults.file, err) |
| } |
| if fileResults.modified && !opts.Quiet { |
| fmt.Fprintf(opts.ErrWriter, "fixed %s\n", fileResults.file) |
| } |
| if fileResults.records != nil { |
| records = append(records, fileResults.records...) |
| } |
| } |
| |
| if opts.IsPrintingProto { |
| data, err := proto.Marshal(&apipb.Output{Records: records}) |
| if err != nil { |
| log.Fatal("marshaling error: ", err) |
| } |
| fmt.Fprintf(opts.OutWriter, "%s", data) |
| } else if opts.IsPrintingJSON { |
| marshaler := jsonpb.Marshaler{} |
| if err := marshaler.Marshal(opts.OutWriter, &apipb.Output{Records: records}); err != nil { |
| log.Fatal("json marshaling error: ", err) |
| } |
| fmt.Fprintln(opts.OutWriter) |
| } else { |
| for _, record := range records { |
| printRecord(opts.OutWriter, record) |
| } |
| } |
| |
| if hasErrors { |
| return 2 |
| } |
| if fileModified || opts.Stdout { |
| return 0 |
| } |
| // The file is not modified, check if there were any non-readonly commands |
| nonReadonlyCommands := false |
| for _, commandsByTarget := range commandsByFile { |
| for _, commands := range commandsByTarget { |
| for _, command := range commands.commands { |
| if _, ok := readonlyCommands[command.tokens[0]]; !ok { |
| nonReadonlyCommands = true |
| break |
| } |
| } |
| } |
| } |
| if nonReadonlyCommands { |
| return 3 |
| } |
| return 0 |
| } |