123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002 |
- /*
- Copyright 2016 Google Inc. All Rights Reserved.
- 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
- http://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 programatically editing BUILD files.
- package edit
- import (
- "bufio"
- "bytes"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "regexp"
- "runtime"
- "strconv"
- "strings"
- apipb "github.com/bazelbuild/buildtools/api_proto"
- "github.com/bazelbuild/buildtools/build"
- "github.com/bazelbuild/buildtools/file"
- "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
- CommandsFile string // file name 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
- }
- // NewOpts returns a new Options struct with some defaults set.
- func NewOpts() *Options {
- return &Options{NumIO: 200, PreferEOLComments: true}
- }
- // Usage is a user-overriden func to print the program usage.
- var Usage = func() {}
- var fileModified = false // set to true when a file has been fixed
- 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.BinaryExpr // 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 := &build.StringExpr{Value: ShortenLabel(val, env.Pkg)}
- AddValueToListAttribute(env.Rule, attr, env.Pkg, strVal, &env.Vars)
- }
- 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")
- str = strings.Replace("# "+str, "\n", "\n# ", -1)
- comment := []build.Comment{{Token: str}}
- // 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.X.Comment().Before = comment
- } else {
- attr.Y.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 := ListFind(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{commentsText(env.Rule.Call.Comments.Before)}},
- }
- case 1: // Print attribute comment.
- attr := env.Rule.AttrDefn(env.Args[0])
- if attr == nil {
- return nil, attrError()
- }
- comments := append(attr.Before, attr.Suffix...)
- env.output.Fields = []*apipb.Output_Record_Field{
- {Value: &apipb.Output_Record_Field_Text{commentsText(comments)}},
- }
- case 2: // Print comment of a specific value in a list.
- attr := env.Rule.Attr(env.Args[0])
- if attr == nil {
- return nil, attrError()
- }
- value := env.Args[1]
- expr := ListFind(attr, value, env.Pkg)
- if expr == nil {
- 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{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.LiteralExpr{Token: kind}}
- rule := &build.Rule{call, ""}
- 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])
- }
- }
- func cmdNewLoad(opts *Options, env CmdEnvironment) (*build.File, error) {
- env.File.Stmt = InsertLoad(env.File.Stmt, env.Args)
- 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{env.Rule.Kind()}}
- } else if str == "name" {
- fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{env.Rule.Name()}}
- } else if str == "label" {
- if env.Rule.Name() != "" {
- fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{fmt.Sprintf("//%s:%s", env.Pkg, env.Rule.Name())}}
- } else {
- return nil, nil
- }
- } else if str == "rule" {
- fields[i] = &apipb.Output_Record_Field{
- Value: &apipb.Output_Record_Field_Text{build.FormatString(env.Rule.Call)},
- }
- } else if str == "startline" {
- fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{int32(env.Rule.Call.ListStart.Line)}}
- } else if str == "endline" {
- fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{int32(env.Rule.Call.End.Pos.Line)}}
- } else if value == nil {
- fmt.Fprintf(os.Stderr, "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{lit.Token}}
- } else if string, ok := value.(*build.StringExpr); ok {
- fields[i] = &apipb.Output_Record_Field{
- Value: &apipb.Output_Record_Field_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{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.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
- }
- }
- if fixed {
- return env.File, nil
- }
- }
- return nil, 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 := env.Args[1]
- newV := 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 LabelsEqual(e.Value, oldV, env.Pkg) {
- env.Rule.SetAttr(key, getAttrValueExpr(key, []string{newV}))
- }
- } 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, getAttrValueExpr(key, []string{newValue}))
- }
- }
- 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))
- }
- 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))
- }
- return env.File, nil
- }
- func getAttrValueExpr(attr string, args []string) 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 _, i := range args {
- list = append(list, &build.StringExpr{Value: i})
- }
- return &build.ListExpr{List: list}
- case IsString(attr):
- return &build.StringExpr{Value: args[0]}
- default:
- return &build.LiteralExpr{Token: args[0]}
- }
- }
- 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)
- }
- 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.Parse("" /* 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 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)
- 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, 2, -1, "<attr> <value(s)>"},
- "new_load": {cmdNewLoad, 1, -1, "<path> <symbol(s)>"},
- "comment": {cmdComment, 1, 3, "<attr>? <value>? <comment>"},
- "print_comment": {cmdPrintComment, 0, 2, "<attr>? <value>?"},
- "delete": {cmdDelete, 0, 0, ""},
- "fix": {cmdFix, 0, -1, "<fix(es)>?"},
- "move": {cmdMove, 3, -1, "<old_attr> <new_attr> <value(s)>"},
- "new": {cmdNew, 2, 4, "<rule_kind> <rule_name> [(before|after) <relative_rule_name>]"},
- "print": {cmdPrint, 0, -1, "<attribute(s)>"},
- "remove": {cmdRemove, 1, -1, "<attr> <value(s)>"},
- "rename": {cmdRename, 2, 2, "<old_attr> <new_attr>"},
- "replace": {cmdReplace, 3, 3, "<attr> <old_value> <new_value>"},
- "substitute": {cmdSubstitute, 3, 3, "<attr> <old_regexp> <new_template>"},
- "set": {cmdSet, 2, -1, "<attr> <value(s)>"},
- "set_if_absent": {cmdSetIfAbsent, 2, -1, "<attr> <value(s)>"},
- "copy": {cmdCopy, 2, 2, "<attr> <from_rule>"},
- "copy_no_overwrite": {cmdCopyNoOverwrite, 2, 2, "<attr> <from_rule>"},
- }
- 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 {
- acceptableType := false
- for _, filterType := range opts.FilterRuleTypes {
- if rule.Kind() == filterType {
- acceptableType = true
- break
- }
- }
- if acceptableType || rule.Kind() == "package" {
- result = append(result, rule)
- }
- }
- 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(name string, cmd CommandInfo, count int) {
- if count >= cmd.MinArg && (cmd.MaxArg == -1 || count <= cmd.MaxArg) {
- return
- }
- if count < cmd.MinArg {
- fmt.Fprintf(os.Stderr, "Too few arguments for command '%s', expected at least %d.\n",
- name, cmd.MinArg)
- } else {
- fmt.Fprintf(os.Stderr, "Too many arguments for command '%s', expected at most %d.\n",
- name, cmd.MaxArg)
- }
- Usage()
- }
- // Match text that only contains spaces if they're escaped with '\'.
- var spaceRegex = regexp.MustCompile(`(\\ |[^ ])+`)
- // SplitOnSpaces behaves like strings.Fields, except that spaces can be escaped.
- // " some dummy\\ string" -> ["some", "dummy string"]
- func SplitOnSpaces(input string) []string {
- result := spaceRegex.FindAllString(input, -1)
- for i, s := range result {
- result[i] = strings.Replace(s, `\ `, " ", -1)
- }
- 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(args []string) (commands []command, targets []string) {
- for _, arg := range args {
- commandTokens := SplitOnSpaces(arg)
- cmd, found := AllCommands[commandTokens[0]]
- if found {
- checkCommandUsage(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 BinaryExpr whose Y value is the assignment "a = v".
- func getGlobalVariables(exprs []build.Expr) (vars map[string]*build.BinaryExpr) {
- vars = make(map[string]*build.BinaryExpr)
- for _, expr := range exprs {
- if binExpr, ok := expr.(*build.BinaryExpr); ok {
- if binExpr.Op != "=" {
- continue
- }
- if lhs, ok := binExpr.X.(*build.LiteralExpr); ok {
- vars[lhs.Token] = binExpr
- }
- }
- }
- 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".
- var buildFileNames = [...]string{"BUILD.bazel", "BUILD", "BUCK"}
- var buildFileNamesSet = map[string]bool{
- "BUILD.bazel": true,
- "BUILD": true,
- "BUCK": 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 = ioutil.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}}
- }
- vars := map[string]*build.BinaryExpr{}
- 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)
- _, pkg, _ := ParseLabel(target)
- if pkg == stdinPackageName { // Special-case: This is already absolute
- absPkg = stdinPackageName
- }
- 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 {
- for _, r := range targets {
- cmdInfo := AllCommands[cmd.tokens[0]]
- 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)
- ndata, err := runBuildifier(opts, f)
- if err != nil {
- return &rewriteResult{file: name, errs: []error{fmt.Errorf("running buildifier: %v", err)}, records: records}
- }
- if opts.Stdout || name == stdinPackageName {
- os.Stdout.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}
- }
- fileModified = true
- 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
- }
- // runBuildifier formats the build file f.
- // Runs opts.Buildifier if it's non-empty, otherwise uses built-in formatter.
- // opts.Buildifier is useful to force consistency with other tools that call Buildifier.
- func runBuildifier(opts *Options, f *build.File) ([]byte, error) {
- if opts.Buildifier == "" {
- build.Rewrite(f, nil)
- return build.Format(f), nil
- }
- cmd := exec.Command(opts.Buildifier)
- data := build.Format(f)
- cmd.Stdin = bytes.NewBuffer(data)
- stdout := bytes.NewBuffer(nil)
- stderr := bytes.NewBuffer(nil)
- cmd.Stdout = stdout
- cmd.Stderr = stderr
- err := cmd.Run()
- if stderr.Len() > 0 {
- return nil, fmt.Errorf("%s", stderr.Bytes())
- }
- if err != nil {
- return nil, err
- }
- return stdout.Bytes(), nil
- }
- // Given a target, whose package may contain a trailing "/...", returns all
- // extisting BUILD file paths which match the package.
- func targetExpressionToBuildFiles(opts *Options, target string) []string {
- file, _, _ := InterpretLabelForWorkspaceLocation(opts.RootDir, target)
- if opts.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)
- }
- }
- if !strings.HasSuffix(file, "/.../BUILD") {
- return []string{file}
- }
- var buildFiles []string
- searchDirs := []string{strings.TrimSuffix(file, "/.../BUILD")}
- for len(searchDirs) != 0 {
- lastIndex := len(searchDirs) - 1
- dir := searchDirs[lastIndex]
- searchDirs = searchDirs[:lastIndex]
- dirFiles, err := ioutil.ReadDir(dir)
- if err != nil {
- continue
- }
- for _, dirFile := range dirFiles {
- if dirFile.IsDir() {
- searchDirs = append(searchDirs, path.Join(dir, dirFile.Name()))
- } else if _, ok := buildFileNamesSet[dirFile.Name()]; ok {
- buildFiles = append(buildFiles, path.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) {
- commands, targets := parseCommands(args)
- for _, target := range targets {
- if strings.HasSuffix(target, "/BUILD") {
- target = strings.TrimSuffix(target, "/BUILD") + ":__pkg__"
- }
- var buildFiles []string
- _, pkg, _ := ParseLabel(target)
- if pkg == stdinPackageName {
- buildFiles = []string{stdinPackageName}
- } else {
- buildFiles = targetExpressionToBuildFiles(opts, target)
- }
- for _, file := range buildFiles {
- commandMap[file] = append(commandMap[file], commandsForTarget{target, commands})
- }
- }
- }
- func appendCommandsFromFile(opts *Options, commandsByFile map[string][]commandsForTarget, fileName string) {
- var reader io.Reader
- if opts.CommandsFile == stdinPackageName {
- reader = os.Stdin
- } else {
- rc := file.OpenReadFile(opts.CommandsFile)
- reader = rc
- defer rc.Close()
- }
- scanner := bufio.NewScanner(reader)
- for scanner.Scan() {
- line := scanner.Text()
- if line == "" {
- continue
- }
- args := strings.Split(line, "|")
- appendCommands(opts, commandsByFile, args)
- }
- if err := scanner.Err(); err != nil {
- fmt.Fprintf(os.Stderr, "Error while reading commands file: %v", scanner.Err())
- }
- }
- 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
- }
- break
- case *apipb.Output_Record_Field_Number:
- line[i] = strconv.Itoa(int(value.Number))
- break
- case *apipb.Output_Record_Field_Error:
- switch value.Error {
- case apipb.Output_Record_Field_UNKNOWN:
- line[i] = "(unknown)"
- break
- case apipb.Output_Record_Field_MISSING:
- line[i] = "(missing)"
- break
- }
- break
- case *apipb.Output_Record_Field_List:
- line[i] = fmt.Sprintf("[%s]", strings.Join(value.List.Strings, " "))
- break
- }
- }
- 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 {
- commandsByFile := make(map[string][]commandsForTarget)
- if opts.CommandsFile != "" {
- appendCommandsFromFile(opts, commandsByFile, opts.CommandsFile)
- } else {
- if len(args) == 0 {
- Usage()
- }
- appendCommands(opts, commandsByFile, args)
- }
- numFiles := len(commandsByFile)
- if opts.Parallelism > 0 {
- runtime.GOMAXPROCS(opts.Parallelism)
- }
- results := make(chan *rewriteResult, numFiles)
- data := make(chan commandsForFile)
- 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{}
- hasErrors := false
- for i := 0; i < numFiles; i++ {
- fileResults := <-results
- if fileResults == nil {
- continue
- }
- hasErrors = hasErrors || len(fileResults.errs) > 0
- for _, err := range fileResults.errs {
- fmt.Fprintf(os.Stderr, "%s: %s\n", fileResults.file, err)
- }
- if fileResults.modified && !opts.Quiet {
- fmt.Fprintf(os.Stderr, "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(os.Stdout, "%s", data)
- } else {
- for _, record := range records {
- printRecord(os.Stdout, record)
- }
- }
- if hasErrors {
- return 2
- }
- if !fileModified && !opts.Stdout {
- return 3
- }
- return 0
- }
|