github.com/cheshirekow/buildtools@v0.0.0-20200224190056-5d637702fe81/edit/buildozer.go (about) 1 /* 2 Copyright 2016 Google Inc. All Rights Reserved. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 http://www.apache.org/licenses/LICENSE-2.0 7 Unless required by applicable law or agreed to in writing, software 8 distributed under the License is distributed on an "AS IS" BASIS, 9 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 See the License for the specific language governing permissions and 11 limitations under the License. 12 */ 13 14 // Buildozer is a tool for programmatically editing BUILD files. 15 16 package edit 17 18 import ( 19 "bufio" 20 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "log" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "regexp" 30 "runtime" 31 "strconv" 32 "strings" 33 34 apipb "github.com/cheshirekow/buildtools/api_proto" 35 "github.com/cheshirekow/buildtools/build" 36 "github.com/cheshirekow/buildtools/file" 37 "github.com/golang/protobuf/proto" 38 ) 39 40 // Options represents choices about how buildozer should behave. 41 type Options struct { 42 Stdout bool // write changed BUILD file to stdout 43 Buildifier string // path to buildifier binary 44 Parallelism int // number of cores to use for concurrent actions 45 NumIO int // number of concurrent actions 46 CommandsFile string // file name to read commands from, use '-' for stdin (format:|-separated command line arguments to buildozer, excluding flags 47 KeepGoing bool // apply all commands, even if there are failures 48 FilterRuleTypes []string // list of rule types to change, empty means all 49 PreferEOLComments bool // when adding a new comment, put it on the same line if possible 50 RootDir string // If present, use this folder rather than $PWD to find the root dir 51 Quiet bool // suppress informational messages. 52 EditVariables bool // for attributes that simply assign a variable (e.g. hdrs = LIB_HDRS), edit the build variable instead of appending to the attribute. 53 IsPrintingProto bool // output serialized devtools.buildozer.Output protos instead of human-readable strings 54 } 55 56 // NewOpts returns a new Options struct with some defaults set. 57 func NewOpts() *Options { 58 return &Options{NumIO: 200, PreferEOLComments: true} 59 } 60 61 // Usage is a user-overridden func to print the program usage. 62 var Usage = func() {} 63 64 var fileModified = false // set to true when a file has been fixed 65 66 const stdinPackageName = "-" // the special package name to represent stdin 67 68 // CmdEnvironment stores the information the commands below have access to. 69 type CmdEnvironment struct { 70 File *build.File // the AST 71 Rule *build.Rule // the rule to modify 72 Vars map[string]*build.AssignExpr // global variables set in the build file 73 Pkg string // the full package name 74 Args []string // the command-line arguments 75 output *apipb.Output_Record // output proto, stores whatever a command wants to print 76 } 77 78 // The cmdXXX functions implement the various commands. 79 80 func cmdAdd(opts *Options, env CmdEnvironment) (*build.File, error) { 81 attr := env.Args[0] 82 for _, val := range env.Args[1:] { 83 if IsIntList(attr) { 84 AddValueToListAttribute(env.Rule, attr, env.Pkg, &build.LiteralExpr{Token: val}, &env.Vars) 85 continue 86 } 87 strVal := getStringExpr(val, env.Pkg) 88 AddValueToListAttribute(env.Rule, attr, env.Pkg, strVal, &env.Vars) 89 } 90 ResolveAttr(env.Rule, attr, env.Pkg) 91 return env.File, nil 92 } 93 94 func cmdComment(opts *Options, env CmdEnvironment) (*build.File, error) { 95 // The comment string is always the last argument in the list. 96 str := env.Args[len(env.Args)-1] 97 str = strings.Replace(str, "\\n", "\n", -1) 98 // Multiline comments should go on a separate line. 99 fullLine := !opts.PreferEOLComments || strings.Contains(str, "\n") 100 comment := []build.Comment{} 101 for _, line := range strings.Split(str, "\n") { 102 comment = append(comment, build.Comment{Token: "# " + line}) 103 } 104 105 // The comment might be attached to a rule, an attribute, or a value in a list, 106 // depending on how many arguments are passed. 107 switch len(env.Args) { 108 case 1: // Attach to a rule 109 env.Rule.Call.Comments.Before = comment 110 case 2: // Attach to an attribute 111 if attr := env.Rule.AttrDefn(env.Args[0]); attr != nil { 112 if fullLine { 113 attr.LHS.Comment().Before = comment 114 } else { 115 attr.RHS.Comment().Suffix = comment 116 } 117 } 118 case 3: // Attach to a specific value in a list 119 if attr := env.Rule.Attr(env.Args[0]); attr != nil { 120 if expr := ListFind(attr, env.Args[1], env.Pkg); expr != nil { 121 if fullLine { 122 expr.Comments.Before = comment 123 } else { 124 expr.Comments.Suffix = comment 125 } 126 } 127 } 128 default: 129 panic("cmdComment") 130 } 131 return env.File, nil 132 } 133 134 // commentsText concatenates comments into a single line. 135 func commentsText(comments []build.Comment) string { 136 var segments []string 137 for _, comment := range comments { 138 token := comment.Token 139 if strings.HasPrefix(token, "#") { 140 token = token[1:] 141 } 142 segments = append(segments, strings.TrimSpace(token)) 143 } 144 return strings.Replace(strings.Join(segments, " "), "\n", " ", -1) 145 } 146 147 func cmdPrintComment(opts *Options, env CmdEnvironment) (*build.File, error) { 148 attrError := func() error { 149 return fmt.Errorf("rule \"//%s:%s\" has no attribute \"%s\"", env.Pkg, env.Rule.Name(), env.Args[0]) 150 } 151 152 switch len(env.Args) { 153 case 0: // Print rule comment. 154 env.output.Fields = []*apipb.Output_Record_Field{ 155 {Value: &apipb.Output_Record_Field_Text{commentsText(env.Rule.Call.Comments.Before)}}, 156 } 157 case 1: // Print attribute comment. 158 attr := env.Rule.AttrDefn(env.Args[0]) 159 if attr == nil { 160 return nil, attrError() 161 } 162 comments := append(attr.Before, attr.Suffix...) 163 env.output.Fields = []*apipb.Output_Record_Field{ 164 {Value: &apipb.Output_Record_Field_Text{commentsText(comments)}}, 165 } 166 case 2: // Print comment of a specific value in a list. 167 attr := env.Rule.Attr(env.Args[0]) 168 if attr == nil { 169 return nil, attrError() 170 } 171 value := env.Args[1] 172 expr := ListFind(attr, value, env.Pkg) 173 if expr == nil { 174 return nil, fmt.Errorf("attribute \"%s\" has no value \"%s\"", env.Args[0], value) 175 } 176 comments := append(expr.Comments.Before, expr.Comments.Suffix...) 177 env.output.Fields = []*apipb.Output_Record_Field{ 178 {Value: &apipb.Output_Record_Field_Text{commentsText(comments)}}, 179 } 180 default: 181 panic("cmdPrintComment") 182 } 183 return nil, nil 184 } 185 186 func cmdDelete(opts *Options, env CmdEnvironment) (*build.File, error) { 187 return DeleteRule(env.File, env.Rule), nil 188 } 189 190 func cmdMove(opts *Options, env CmdEnvironment) (*build.File, error) { 191 oldAttr := env.Args[0] 192 newAttr := env.Args[1] 193 if len(env.Args) == 3 && env.Args[2] == "*" { 194 if err := MoveAllListAttributeValues(env.Rule, oldAttr, newAttr, env.Pkg, &env.Vars); err != nil { 195 return nil, err 196 } 197 return env.File, nil 198 } 199 fixed := false 200 for _, val := range env.Args[2:] { 201 if deleted := ListAttributeDelete(env.Rule, oldAttr, val, env.Pkg); deleted != nil { 202 AddValueToListAttribute(env.Rule, newAttr, env.Pkg, deleted, &env.Vars) 203 fixed = true 204 } 205 } 206 if fixed { 207 return env.File, nil 208 } 209 return nil, nil 210 } 211 212 func cmdNew(opts *Options, env CmdEnvironment) (*build.File, error) { 213 kind := env.Args[0] 214 name := env.Args[1] 215 addAtEOF, insertionIndex, err := findInsertionIndex(env) 216 if err != nil { 217 return nil, err 218 } 219 220 if FindRuleByName(env.File, name) != nil { 221 return nil, fmt.Errorf("rule '%s' already exists", name) 222 } 223 224 call := &build.CallExpr{X: &build.Ident{Name: kind}} 225 rule := &build.Rule{call, ""} 226 rule.SetAttr("name", &build.StringExpr{Value: name}) 227 228 if addAtEOF { 229 env.File.Stmt = InsertAfterLastOfSameKind(env.File.Stmt, rule.Call) 230 } else { 231 env.File.Stmt = InsertAfter(insertionIndex, env.File.Stmt, call) 232 } 233 return env.File, nil 234 } 235 236 // findInsertionIndex is used by cmdNew to find the place at which to insert the new rule. 237 func findInsertionIndex(env CmdEnvironment) (bool, int, error) { 238 if len(env.Args) < 4 { 239 return true, 0, nil 240 } 241 242 relativeToRuleName := env.Args[3] 243 ruleIdx, _ := IndexOfRuleByName(env.File, relativeToRuleName) 244 if ruleIdx == -1 { 245 return true, 0, nil 246 } 247 248 switch env.Args[2] { 249 case "before": 250 return false, ruleIdx - 1, nil 251 case "after": 252 return false, ruleIdx, nil 253 default: 254 return true, 0, fmt.Errorf("Unknown relative operator '%s'; allowed: 'before', 'after'", env.Args[1]) 255 } 256 } 257 258 // splitLoadArgs splits arguments of form <[to=]from> 259 // into a slice of froms and a slice of tos. 260 func splitLoadArgs(args []string) ([]string, []string) { 261 from := args 262 to := append([]string{}, args...) 263 for i := range from { 264 if s := strings.SplitN(from[i], "=", 2); len(s) == 2 { 265 to[i] = s[0] 266 from[i] = s[1] 267 } 268 } 269 270 return from, to 271 } 272 273 func cmdNewLoad(opts *Options, env CmdEnvironment) (*build.File, error) { 274 from, to := splitLoadArgs(env.Args[1:]) 275 env.File.Stmt = InsertLoad(env.File.Stmt, env.Args[0], from, to) 276 return env.File, nil 277 } 278 279 func cmdReplaceLoad(opts *Options, env CmdEnvironment) (*build.File, error) { 280 from, to := splitLoadArgs(env.Args[1:]) 281 env.File.Stmt = ReplaceLoad(env.File.Stmt, env.Args[0], from, to) 282 return env.File, nil 283 } 284 285 func cmdSubstituteLoad(opts *Options, env CmdEnvironment) (*build.File, error) { 286 oldRegexp, err := regexp.Compile(env.Args[0]) 287 if err != nil { 288 return nil, err 289 } 290 newTemplate := env.Args[1] 291 292 for _, stmt := range env.File.Stmt { 293 load, ok := stmt.(*build.LoadStmt) 294 if !ok { 295 continue 296 } 297 298 if newValue, ok := stringSubstitute(load.Module.Value, oldRegexp, newTemplate); ok { 299 load.Module.Value = newValue 300 } 301 } 302 303 return env.File, nil 304 } 305 306 func cmdPrint(opts *Options, env CmdEnvironment) (*build.File, error) { 307 format := env.Args 308 if len(format) == 0 { 309 format = []string{"name", "kind"} 310 } 311 fields := make([]*apipb.Output_Record_Field, len(format)) 312 313 for i, str := range format { 314 value := env.Rule.Attr(str) 315 if str == "kind" { 316 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{env.Rule.Kind()}} 317 } else if str == "name" { 318 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{env.Rule.Name()}} 319 } else if str == "label" { 320 if env.Rule.Name() != "" { 321 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{fmt.Sprintf("//%s:%s", env.Pkg, env.Rule.Name())}} 322 } else { 323 return nil, nil 324 } 325 } else if str == "rule" { 326 fields[i] = &apipb.Output_Record_Field{ 327 Value: &apipb.Output_Record_Field_Text{build.FormatString(env.Rule.Call)}, 328 } 329 } else if str == "startline" { 330 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{int32(env.Rule.Call.ListStart.Line)}} 331 } else if str == "endline" { 332 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Number{int32(env.Rule.Call.End.Pos.Line)}} 333 } else if value == nil { 334 fmt.Fprintf(os.Stderr, "rule \"//%s:%s\" has no attribute \"%s\"\n", 335 env.Pkg, env.Rule.Name(), str) 336 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Error{Error: apipb.Output_Record_Field_MISSING}} 337 } else if lit, ok := value.(*build.LiteralExpr); ok { 338 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{lit.Token}} 339 } else if lit, ok := value.(*build.Ident); ok { 340 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{lit.Name}} 341 } else if string, ok := value.(*build.StringExpr); ok { 342 fields[i] = &apipb.Output_Record_Field{ 343 Value: &apipb.Output_Record_Field_Text{string.Value}, 344 QuoteWhenPrinting: true, 345 } 346 } else if strList := env.Rule.AttrStrings(str); strList != nil { 347 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_List{List: &apipb.RepeatedString{Strings: strList}}} 348 } else { 349 // Some other Expr we haven't listed above. Just print it. 350 fields[i] = &apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{build.FormatString(value)}} 351 } 352 } 353 354 env.output.Fields = fields 355 return nil, nil 356 } 357 358 func attrKeysForPattern(rule *build.Rule, pattern string) []string { 359 if pattern == "*" { 360 return rule.AttrKeys() 361 } 362 return []string{pattern} 363 } 364 365 func cmdRemove(opts *Options, env CmdEnvironment) (*build.File, error) { 366 if len(env.Args) == 1 { // Remove the attribute 367 if env.Rule.DelAttr(env.Args[0]) != nil { 368 return env.File, nil 369 } 370 } else { // Remove values in the attribute. 371 fixed := false 372 for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { 373 for _, val := range env.Args[1:] { 374 ListAttributeDelete(env.Rule, key, val, env.Pkg) 375 fixed = true 376 } 377 ResolveAttr(env.Rule, key, env.Pkg) 378 } 379 if fixed { 380 return env.File, nil 381 } 382 } 383 return nil, nil 384 } 385 386 func cmdRemoveComment(opts *Options, env CmdEnvironment) (*build.File, error) { 387 switch len(env.Args) { 388 case 0: // Remove comment attached to rule 389 env.Rule.Call.Comments.Before = nil 390 env.Rule.Call.Comments.Suffix = nil 391 env.Rule.Call.Comments.After = nil 392 case 1: // Remove comment attached to attr 393 if attr := env.Rule.AttrDefn(env.Args[0]); attr != nil { 394 attr.Comments.Before = nil 395 attr.Comments.Suffix = nil 396 attr.Comments.After = nil 397 attr.LHS.Comment().Before = nil 398 attr.LHS.Comment().Suffix = nil 399 attr.LHS.Comment().After = nil 400 attr.RHS.Comment().Before = nil 401 attr.RHS.Comment().Suffix = nil 402 attr.RHS.Comment().After = nil 403 } 404 case 2: // Remove comment attached to value 405 if attr := env.Rule.Attr(env.Args[0]); attr != nil { 406 if expr := ListFind(attr, env.Args[1], env.Pkg); expr != nil { 407 expr.Comments.Before = nil 408 expr.Comments.Suffix = nil 409 expr.Comments.After = nil 410 } 411 } 412 default: 413 panic("cmdRemoveComment") 414 } 415 return env.File, nil 416 } 417 418 func cmdRename(opts *Options, env CmdEnvironment) (*build.File, error) { 419 oldAttr := env.Args[0] 420 newAttr := env.Args[1] 421 if err := RenameAttribute(env.Rule, oldAttr, newAttr); err != nil { 422 return nil, err 423 } 424 return env.File, nil 425 } 426 427 func cmdReplace(opts *Options, env CmdEnvironment) (*build.File, error) { 428 oldV := env.Args[1] 429 newV := env.Args[2] 430 for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { 431 attr := env.Rule.Attr(key) 432 if e, ok := attr.(*build.StringExpr); ok { 433 if LabelsEqual(e.Value, oldV, env.Pkg) { 434 env.Rule.SetAttr(key, getAttrValueExpr(key, []string{newV}, env)) 435 } 436 } else { 437 ListReplace(attr, oldV, newV, env.Pkg) 438 } 439 } 440 return env.File, nil 441 } 442 443 func cmdSubstitute(opts *Options, env CmdEnvironment) (*build.File, error) { 444 oldRegexp, err := regexp.Compile(env.Args[1]) 445 if err != nil { 446 return nil, err 447 } 448 newTemplate := env.Args[2] 449 for _, key := range attrKeysForPattern(env.Rule, env.Args[0]) { 450 attr := env.Rule.Attr(key) 451 e, ok := attr.(*build.StringExpr) 452 if !ok { 453 ListSubstitute(attr, oldRegexp, newTemplate) 454 continue 455 } 456 if newValue, ok := stringSubstitute(e.Value, oldRegexp, newTemplate); ok { 457 env.Rule.SetAttr(key, getAttrValueExpr(key, []string{newValue}, env)) 458 } 459 } 460 return env.File, nil 461 } 462 463 func cmdSet(opts *Options, env CmdEnvironment) (*build.File, error) { 464 attr := env.Args[0] 465 args := env.Args[1:] 466 if attr == "kind" { 467 env.Rule.SetKind(args[0]) 468 } else { 469 env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env)) 470 } 471 return env.File, nil 472 } 473 474 func cmdSetIfAbsent(opts *Options, env CmdEnvironment) (*build.File, error) { 475 attr := env.Args[0] 476 args := env.Args[1:] 477 if attr == "kind" { 478 return nil, fmt.Errorf("setting 'kind' is not allowed for set_if_absent. Got %s", env.Args) 479 } 480 if env.Rule.Attr(attr) == nil { 481 env.Rule.SetAttr(attr, getAttrValueExpr(attr, args, env)) 482 } 483 return env.File, nil 484 } 485 486 func getAttrValueExpr(attr string, args []string, env CmdEnvironment) build.Expr { 487 switch { 488 case attr == "kind": 489 return nil 490 case IsIntList(attr): 491 var list []build.Expr 492 for _, i := range args { 493 list = append(list, &build.LiteralExpr{Token: i}) 494 } 495 return &build.ListExpr{List: list} 496 case IsList(attr) && !(len(args) == 1 && strings.HasPrefix(args[0], "glob(")): 497 var list []build.Expr 498 for _, arg := range args { 499 list = append(list, getStringExpr(arg, env.Pkg)) 500 } 501 return &build.ListExpr{List: list} 502 case len(args) == 0: 503 // Expected a non-list argument, nothing provided 504 return &build.Ident{Name: "None"} 505 case IsString(attr): 506 return getStringExpr(args[0], env.Pkg) 507 default: 508 return &build.Ident{Name: args[0]} 509 } 510 } 511 512 func getStringExpr(value, pkg string) build.Expr { 513 unquoted, triple, err := build.Unquote(value) 514 if err == nil { 515 return &build.StringExpr{Value: ShortenLabel(unquoted, pkg), TripleQuote: triple} 516 } 517 return &build.StringExpr{Value: ShortenLabel(value, pkg)} 518 } 519 520 func cmdCopy(opts *Options, env CmdEnvironment) (*build.File, error) { 521 attrName := env.Args[0] 522 from := env.Args[1] 523 524 return copyAttributeBetweenRules(env, attrName, from) 525 } 526 527 func cmdCopyNoOverwrite(opts *Options, env CmdEnvironment) (*build.File, error) { 528 attrName := env.Args[0] 529 from := env.Args[1] 530 531 if env.Rule.Attr(attrName) != nil { 532 return env.File, nil 533 } 534 535 return copyAttributeBetweenRules(env, attrName, from) 536 } 537 538 // cmdDictAdd adds a key to a dict, if that key does _not_ exit already. 539 func cmdDictAdd(opts *Options, env CmdEnvironment) (*build.File, error) { 540 attr := env.Args[0] 541 args := env.Args[1:] 542 543 dict := &build.DictExpr{} 544 currDict, ok := env.Rule.Attr(attr).(*build.DictExpr) 545 if ok { 546 dict = currDict 547 } 548 549 for _, x := range args { 550 kv := strings.SplitN(x, ":", 2) 551 expr := getStringExpr(kv[1], env.Pkg) 552 553 prev := DictionaryGet(dict, kv[0]) 554 if prev == nil { 555 // Only set the value if the value is currently unset. 556 DictionarySet(dict, kv[0], expr) 557 } 558 } 559 env.Rule.SetAttr(attr, dict) 560 return env.File, nil 561 } 562 563 // cmdDictSet adds a key to a dict, overwriting any previous values. 564 func cmdDictSet(opts *Options, env CmdEnvironment) (*build.File, error) { 565 attr := env.Args[0] 566 args := env.Args[1:] 567 568 dict := &build.DictExpr{} 569 currDict, ok := env.Rule.Attr(attr).(*build.DictExpr) 570 if ok { 571 dict = currDict 572 } 573 574 for _, x := range args { 575 kv := strings.SplitN(x, ":", 2) 576 expr := getStringExpr(kv[1], env.Pkg) 577 // Set overwrites previous values. 578 DictionarySet(dict, kv[0], expr) 579 } 580 env.Rule.SetAttr(attr, dict) 581 return env.File, nil 582 } 583 584 // cmdDictRemove removes a key from a dict. 585 func cmdDictRemove(opts *Options, env CmdEnvironment) (*build.File, error) { 586 attr := env.Args[0] 587 args := env.Args[1:] 588 589 thing := env.Rule.Attr(attr) 590 dictAttr, ok := thing.(*build.DictExpr) 591 if !ok { 592 return env.File, nil 593 } 594 595 for _, x := range args { 596 // should errors here be flagged? 597 DictionaryDelete(dictAttr, x) 598 env.Rule.SetAttr(attr, dictAttr) 599 } 600 601 // If the removal results in the dict having no contents, delete the attribute (stay clean!) 602 if dictAttr == nil || len(dictAttr.List) == 0 { 603 env.Rule.DelAttr(attr) 604 } 605 606 return env.File, nil 607 } 608 609 // cmdDictListAdd adds an item to a list in a dict. 610 func cmdDictListAdd(opts *Options, env CmdEnvironment) (*build.File, error) { 611 attr := env.Args[0] 612 key := env.Args[1] 613 args := env.Args[2:] 614 615 dict := &build.DictExpr{} 616 if currDict, ok := env.Rule.Attr(attr).(*build.DictExpr); ok { 617 dict = currDict 618 } 619 620 prev := DictionaryGet(dict, key) 621 if prev == nil { 622 prev = &build.ListExpr{} 623 } 624 625 for _, val := range args { 626 expr := getStringExpr(val, env.Pkg) 627 prev = AddValueToList(prev, env.Pkg, expr, true) 628 } 629 630 DictionarySet(dict, key, prev) 631 env.Rule.SetAttr(attr, dict) 632 633 return env.File, nil 634 } 635 636 func copyAttributeBetweenRules(env CmdEnvironment, attrName string, from string) (*build.File, error) { 637 fromRule := FindRuleByName(env.File, from) 638 if fromRule == nil { 639 return nil, fmt.Errorf("could not find rule '%s'", from) 640 } 641 attr := fromRule.Attr(attrName) 642 if attr == nil { 643 return nil, fmt.Errorf("rule '%s' does not have attribute '%s'", from, attrName) 644 } 645 646 ast, err := build.ParseBuild("" /* filename */, []byte(build.FormatString(attr))) 647 if err != nil { 648 return nil, fmt.Errorf("could not parse attribute value %v", build.FormatString(attr)) 649 } 650 651 env.Rule.SetAttr(attrName, ast.Stmt[0]) 652 return env.File, nil 653 } 654 655 func cmdFix(opts *Options, env CmdEnvironment) (*build.File, error) { 656 // Fix the whole file 657 if env.Rule.Kind() == "package" { 658 return FixFile(env.File, env.Pkg, env.Args), nil 659 } 660 // Fix a specific rule 661 return FixRule(env.File, env.Pkg, env.Rule, env.Args), nil 662 } 663 664 // CommandInfo provides a command function and info on incoming arguments. 665 type CommandInfo struct { 666 Fn func(*Options, CmdEnvironment) (*build.File, error) 667 PerRule bool 668 MinArg int 669 MaxArg int 670 Template string 671 } 672 673 // AllCommands associates the command names with their function and number 674 // of arguments. 675 var AllCommands = map[string]CommandInfo{ 676 "add": {cmdAdd, true, 2, -1, "<attr> <value(s)>"}, 677 "new_load": {cmdNewLoad, false, 1, -1, "<path> <[to=]from(s)>"}, 678 "replace_load": {cmdReplaceLoad, false, 1, -1, "<path> <[to=]symbol(s)>"}, 679 "substitute_load": {cmdSubstituteLoad, false, 2, 2, "<old_regexp> <new_template>"}, 680 "comment": {cmdComment, true, 1, 3, "<attr>? <value>? <comment>"}, 681 "print_comment": {cmdPrintComment, true, 0, 2, "<attr>? <value>?"}, 682 "delete": {cmdDelete, true, 0, 0, ""}, 683 "fix": {cmdFix, true, 0, -1, "<fix(es)>?"}, 684 "move": {cmdMove, true, 3, -1, "<old_attr> <new_attr> <value(s)>"}, 685 "new": {cmdNew, false, 2, 4, "<rule_kind> <rule_name> [(before|after) <relative_rule_name>]"}, 686 "print": {cmdPrint, true, 0, -1, "<attribute(s)>"}, 687 "remove": {cmdRemove, true, 1, -1, "<attr> <value(s)>"}, 688 "remove_comment": {cmdRemoveComment, true, 0, 2, "<attr>? <value>?"}, 689 "rename": {cmdRename, true, 2, 2, "<old_attr> <new_attr>"}, 690 "replace": {cmdReplace, true, 3, 3, "<attr> <old_value> <new_value>"}, 691 "substitute": {cmdSubstitute, true, 3, 3, "<attr> <old_regexp> <new_template>"}, 692 "set": {cmdSet, true, 1, -1, "<attr> <value(s)>"}, 693 "set_if_absent": {cmdSetIfAbsent, true, 1, -1, "<attr> <value(s)>"}, 694 "copy": {cmdCopy, true, 2, 2, "<attr> <from_rule>"}, 695 "copy_no_overwrite": {cmdCopyNoOverwrite, true, 2, 2, "<attr> <from_rule>"}, 696 "dict_add": {cmdDictAdd, true, 2, -1, "<attr> <(key:value)(s)>"}, 697 "dict_set": {cmdDictSet, true, 2, -1, "<attr> <(key:value)(s)>"}, 698 "dict_remove": {cmdDictRemove, true, 2, -1, "<attr> <key(s)>"}, 699 "dict_list_add": {cmdDictListAdd, true, 3, -1, "<attr> <key> <value(s)>"}, 700 } 701 702 func expandTargets(f *build.File, rule string) ([]*build.Rule, error) { 703 if r := FindRuleByName(f, rule); r != nil { 704 return []*build.Rule{r}, nil 705 } else if r := FindExportedFile(f, rule); r != nil { 706 return []*build.Rule{r}, nil 707 } else if rule == "all" || rule == "*" { 708 // "all" is a valid name, it is a wildcard only if no such rule is found. 709 return f.Rules(""), nil 710 } else if strings.HasPrefix(rule, "%") { 711 // "%java_library" will match all java_library functions in the package 712 // "%<LINENUM>" will match the rule which begins at LINENUM. 713 // This is for convenience, "%" is not a valid character in bazel targets. 714 kind := rule[1:] 715 if linenum, err := strconv.Atoi(kind); err == nil { 716 if r := f.RuleAt(linenum); r != nil { 717 return []*build.Rule{r}, nil 718 } 719 } else { 720 return f.Rules(kind), nil 721 } 722 } 723 return nil, fmt.Errorf("rule '%s' not found", rule) 724 } 725 726 func filterRules(opts *Options, rules []*build.Rule) (result []*build.Rule) { 727 if len(opts.FilterRuleTypes) == 0 { 728 return rules 729 } 730 for _, rule := range rules { 731 for _, filterType := range opts.FilterRuleTypes { 732 if rule.Kind() == filterType { 733 result = append(result, rule) 734 break 735 } 736 } 737 } 738 return 739 } 740 741 // command contains a list of tokens that describe a buildozer command. 742 type command struct { 743 tokens []string 744 } 745 746 // checkCommandUsage checks the number of argument of a command. 747 // It prints an error and usage when it is not valid. 748 func checkCommandUsage(name string, cmd CommandInfo, count int) { 749 if count >= cmd.MinArg && (cmd.MaxArg == -1 || count <= cmd.MaxArg) { 750 return 751 } 752 753 if count < cmd.MinArg { 754 fmt.Fprintf(os.Stderr, "Too few arguments for command '%s', expected at least %d.\n", 755 name, cmd.MinArg) 756 } else { 757 fmt.Fprintf(os.Stderr, "Too many arguments for command '%s', expected at most %d.\n", 758 name, cmd.MaxArg) 759 } 760 Usage() 761 os.Exit(1) 762 } 763 764 // Match text that only contains spaces or line breaks if they're escaped with '\'. 765 var spaceRegex = regexp.MustCompile(`(\\ |\\\n|[^ \n])+`) 766 767 // SplitOnSpaces behaves like strings.Fields, except that spaces can be escaped. 768 // Also splits on linebreaks unless they are escaped too. 769 // " some dummy\\ string" -> ["some", "dummy string"] 770 func SplitOnSpaces(input string) []string { 771 result := spaceRegex.FindAllString(input, -1) 772 for i, s := range result { 773 s = strings.Replace(s, `\ `, " ", -1) 774 s = strings.Replace(s, "\\\n", "\n", -1) 775 result[i] = s 776 } 777 return result 778 } 779 780 // parseCommands parses commands and targets they should be applied on from 781 // a list of arguments. 782 // Each argument can be either: 783 // - a command (as defined by AllCommands) and its parameters, separated by 784 // whitespace 785 // - a target all commands that are parsed during one call to parseCommands 786 // should be applied on 787 func parseCommands(args []string) (commands []command, targets []string) { 788 for _, arg := range args { 789 commandTokens := SplitOnSpaces(arg) 790 cmd, found := AllCommands[commandTokens[0]] 791 if found { 792 checkCommandUsage(commandTokens[0], cmd, len(commandTokens)-1) 793 commands = append(commands, command{commandTokens}) 794 } else { 795 targets = append(targets, arg) 796 } 797 } 798 return 799 } 800 801 // commandsForTarget contains commands to be executed on the given target. 802 type commandsForTarget struct { 803 target string 804 commands []command 805 } 806 807 // commandsForFile contains the file name and all commands that should be 808 // applied on that file, indexed by their target. 809 type commandsForFile struct { 810 file string 811 commands []commandsForTarget 812 } 813 814 // commandError returns an error that formats 'err' in the context of the 815 // commands to be executed on the given target. 816 func commandError(commands []command, target string, err error) error { 817 return fmt.Errorf("error while executing commands %s on target %s: %s", commands, target, err) 818 } 819 820 // rewriteResult contains the outcome of applying fixes to a single file. 821 type rewriteResult struct { 822 file string 823 errs []error 824 modified bool 825 records []*apipb.Output_Record 826 } 827 828 // getGlobalVariables returns the global variable assignments in the provided list of expressions. 829 // That is, for each variable assignment of the form 830 // a = v 831 // vars["a"] will contain the AssignExpr whose RHS value is the assignment "a = v". 832 func getGlobalVariables(exprs []build.Expr) (vars map[string]*build.AssignExpr) { 833 vars = make(map[string]*build.AssignExpr) 834 for _, expr := range exprs { 835 if as, ok := expr.(*build.AssignExpr); ok { 836 if lhs, ok := as.LHS.(*build.Ident); ok { 837 vars[lhs.Name] = as 838 } 839 } 840 } 841 return vars 842 } 843 844 // When checking the filesystem, we need to look for any of the 845 // possible buildFileNames. For historical reasons, the 846 // parts of the tool that generate paths that we may want to examine 847 // continue to assume that build files are all named "BUILD". 848 var buildFileNames = [...]string{"BUILD.bazel", "BUILD", "BUCK"} 849 var buildFileNamesSet = map[string]bool{ 850 "BUILD.bazel": true, 851 "BUILD": true, 852 "BUCK": true, 853 } 854 855 // rewrite parses the BUILD file for the given file, transforms the AST, 856 // and write the changes back in the file (or on stdout). 857 func rewrite(opts *Options, commandsForFile commandsForFile) *rewriteResult { 858 name := commandsForFile.file 859 var data []byte 860 var err error 861 var fi os.FileInfo 862 records := []*apipb.Output_Record{} 863 if name == stdinPackageName { // read on stdin 864 data, err = ioutil.ReadAll(os.Stdin) 865 if err != nil { 866 return &rewriteResult{file: name, errs: []error{err}} 867 } 868 } else { 869 origName := name 870 for _, suffix := range buildFileNames { 871 if strings.HasSuffix(name, "/"+suffix) { 872 name = strings.TrimSuffix(name, suffix) 873 break 874 } 875 } 876 for _, suffix := range buildFileNames { 877 name = name + suffix 878 data, fi, err = file.ReadFile(name) 879 if err == nil { 880 break 881 } 882 name = strings.TrimSuffix(name, suffix) 883 } 884 if err != nil { 885 data, fi, err = file.ReadFile(name) 886 } 887 if err != nil { 888 err = errors.New("file not found or not readable") 889 return &rewriteResult{file: origName, errs: []error{err}} 890 } 891 } 892 893 f, err := build.ParseBuild(name, data) 894 if err != nil { 895 return &rewriteResult{file: name, errs: []error{err}} 896 } 897 898 vars := map[string]*build.AssignExpr{} 899 if opts.EditVariables { 900 vars = getGlobalVariables(f.Stmt) 901 } 902 var errs []error 903 changed := false 904 for _, commands := range commandsForFile.commands { 905 target := commands.target 906 commands := commands.commands 907 _, absPkg, rule := InterpretLabelForWorkspaceLocation(opts.RootDir, target) 908 _, pkg, _ := ParseLabel(target) 909 if pkg == stdinPackageName { // Special-case: This is already absolute 910 absPkg = stdinPackageName 911 } 912 913 targets, err := expandTargets(f, rule) 914 if err != nil { 915 cerr := commandError(commands, target, err) 916 errs = append(errs, cerr) 917 if !opts.KeepGoing { 918 return &rewriteResult{file: name, errs: errs, records: records} 919 920 } 921 } 922 targets = filterRules(opts, targets) 923 for _, cmd := range commands { 924 cmdInfo := AllCommands[cmd.tokens[0]] 925 // Depending on whether a transformation is rule-specific or not, it should be applied to 926 // every rule that satisfies the filter or just once to the file. 927 cmdTargets := targets 928 if !cmdInfo.PerRule { 929 cmdTargets = []*build.Rule{nil} 930 } 931 for _, r := range cmdTargets { 932 record := &apipb.Output_Record{} 933 newf, err := cmdInfo.Fn(opts, CmdEnvironment{f, r, vars, absPkg, cmd.tokens[1:], record}) 934 if len(record.Fields) != 0 { 935 records = append(records, record) 936 } 937 if err != nil { 938 cerr := commandError([]command{cmd}, target, err) 939 if opts.KeepGoing { 940 errs = append(errs, cerr) 941 } else { 942 return &rewriteResult{file: name, errs: []error{cerr}, records: records} 943 } 944 } 945 if newf != nil { 946 changed = true 947 f = newf 948 } 949 } 950 } 951 } 952 if !changed { 953 return &rewriteResult{file: name, errs: errs, records: records} 954 } 955 f = RemoveEmptyPackage(f) 956 ndata, err := runBuildifier(opts, f) 957 if err != nil { 958 return &rewriteResult{file: name, errs: []error{fmt.Errorf("running buildifier: %v", err)}, records: records} 959 } 960 961 if opts.Stdout || name == stdinPackageName { 962 os.Stdout.Write(ndata) 963 return &rewriteResult{file: name, errs: errs, records: records} 964 } 965 966 if bytes.Equal(data, ndata) { 967 return &rewriteResult{file: name, errs: errs, records: records} 968 } 969 970 if err := EditFile(fi, name); err != nil { 971 return &rewriteResult{file: name, errs: []error{err}, records: records} 972 } 973 974 if err := file.WriteFile(name, ndata); err != nil { 975 return &rewriteResult{file: name, errs: []error{err}, records: records} 976 } 977 978 fileModified = true 979 return &rewriteResult{file: name, errs: errs, modified: true, records: records} 980 } 981 982 // EditFile is a function that does any prework needed before editing a file. 983 // e.g. "checking out for write" from a locking source control repo. 984 var EditFile = func(fi os.FileInfo, name string) error { 985 return nil 986 } 987 988 // runBuildifier formats the build file f. 989 // Runs opts.Buildifier if it's non-empty, otherwise uses built-in formatter. 990 // opts.Buildifier is useful to force consistency with other tools that call Buildifier. 991 func runBuildifier(opts *Options, f *build.File) ([]byte, error) { 992 if opts.Buildifier == "" { 993 build.Rewrite(f, nil) 994 return build.Format(f), nil 995 } 996 997 cmd := exec.Command(opts.Buildifier, "--type=build") 998 data := build.Format(f) 999 cmd.Stdin = bytes.NewBuffer(data) 1000 stdout := bytes.NewBuffer(nil) 1001 stderr := bytes.NewBuffer(nil) 1002 cmd.Stdout = stdout 1003 cmd.Stderr = stderr 1004 err := cmd.Run() 1005 if stderr.Len() > 0 { 1006 return nil, fmt.Errorf("%s", stderr.Bytes()) 1007 } 1008 if err != nil { 1009 return nil, err 1010 } 1011 return stdout.Bytes(), nil 1012 } 1013 1014 // Given a target, whose package may contain a trailing "/...", returns all 1015 // existing BUILD file paths which match the package. 1016 func targetExpressionToBuildFiles(rootDir string, target string) []string { 1017 file, _, _ := InterpretLabelForWorkspaceLocation(rootDir, target) 1018 if rootDir == "" { 1019 var err error 1020 if file, err = filepath.Abs(file); err != nil { 1021 fmt.Printf("Cannot make path absolute: %s\n", err.Error()) 1022 os.Exit(1) 1023 } 1024 } 1025 1026 suffix := filepath.Join("", "...", "BUILD") // /.../BUILD 1027 if !strings.HasSuffix(file, suffix) { 1028 return []string{file} 1029 } 1030 1031 return findBuildFiles(strings.TrimSuffix(file, suffix)) 1032 } 1033 1034 // Given a root directory, returns all "BUILD" files in that subtree recursively. 1035 func findBuildFiles(rootDir string) []string { 1036 var buildFiles []string 1037 searchDirs := []string{rootDir} 1038 1039 for len(searchDirs) != 0 { 1040 lastIndex := len(searchDirs) - 1 1041 dir := searchDirs[lastIndex] 1042 searchDirs = searchDirs[:lastIndex] 1043 1044 dirFiles, err := ioutil.ReadDir(dir) 1045 if err != nil { 1046 continue 1047 } 1048 1049 for _, dirFile := range dirFiles { 1050 if dirFile.IsDir() { 1051 searchDirs = append(searchDirs, filepath.Join(dir, dirFile.Name())) 1052 } else if _, ok := buildFileNamesSet[dirFile.Name()]; ok { 1053 buildFiles = append(buildFiles, filepath.Join(dir, dirFile.Name())) 1054 } 1055 } 1056 } 1057 1058 return buildFiles 1059 } 1060 1061 // appendCommands adds the given commands to be applied to each of the given targets 1062 // via the commandMap. 1063 func appendCommands(opts *Options, commandMap map[string][]commandsForTarget, args []string) { 1064 commands, targets := parseCommands(args) 1065 for _, target := range targets { 1066 for _, buildFileName := range buildFileNames { 1067 if strings.HasSuffix(target, filepath.FromSlash("/"+buildFileName)) { 1068 target = strings.TrimSuffix(target, filepath.FromSlash("/"+buildFileName)) + ":__pkg__" 1069 } 1070 } 1071 var buildFiles []string 1072 _, pkg, _ := ParseLabel(target) 1073 if pkg == stdinPackageName { 1074 buildFiles = []string{stdinPackageName} 1075 } else { 1076 buildFiles = targetExpressionToBuildFiles(opts.RootDir, target) 1077 } 1078 1079 for _, file := range buildFiles { 1080 commandMap[file] = append(commandMap[file], commandsForTarget{target, commands}) 1081 } 1082 } 1083 } 1084 1085 func appendCommandsFromFile(opts *Options, commandsByFile map[string][]commandsForTarget, fileName string) { 1086 var reader io.Reader 1087 if opts.CommandsFile == stdinPackageName { 1088 reader = os.Stdin 1089 } else { 1090 rc := file.OpenReadFile(opts.CommandsFile) 1091 reader = rc 1092 defer rc.Close() 1093 } 1094 appendCommandsFromReader(opts, reader, commandsByFile) 1095 } 1096 1097 func appendCommandsFromReader(opts *Options, reader io.Reader, commandsByFile map[string][]commandsForTarget) { 1098 r := bufio.NewReader(reader) 1099 atEOF := false 1100 for !atEOF { 1101 line, err := r.ReadString('\n') 1102 if err == io.EOF { 1103 atEOF = true 1104 err = nil 1105 } 1106 if err != nil { 1107 fmt.Fprintf(os.Stderr, "Error while reading commands file: %v", err) 1108 return 1109 } 1110 line = strings.TrimSuffix(line, "\n") 1111 if line == "" { 1112 continue 1113 } 1114 args := strings.Split(line, "|") 1115 appendCommands(opts, commandsByFile, args) 1116 } 1117 } 1118 1119 func printRecord(writer io.Writer, record *apipb.Output_Record) { 1120 fields := record.Fields 1121 line := make([]string, len(fields)) 1122 for i, field := range fields { 1123 switch value := field.Value.(type) { 1124 case *apipb.Output_Record_Field_Text: 1125 if field.QuoteWhenPrinting && strings.ContainsRune(value.Text, ' ') { 1126 line[i] = fmt.Sprintf("%q", value.Text) 1127 } else { 1128 line[i] = value.Text 1129 } 1130 break 1131 case *apipb.Output_Record_Field_Number: 1132 line[i] = strconv.Itoa(int(value.Number)) 1133 break 1134 case *apipb.Output_Record_Field_Error: 1135 switch value.Error { 1136 case apipb.Output_Record_Field_UNKNOWN: 1137 line[i] = "(unknown)" 1138 break 1139 case apipb.Output_Record_Field_MISSING: 1140 line[i] = "(missing)" 1141 break 1142 } 1143 break 1144 case *apipb.Output_Record_Field_List: 1145 line[i] = fmt.Sprintf("[%s]", strings.Join(value.List.Strings, " ")) 1146 break 1147 } 1148 } 1149 1150 fmt.Fprint(writer, strings.Join(line, " ")+"\n") 1151 } 1152 1153 // Buildozer loops over all arguments on the command line fixing BUILD files. 1154 func Buildozer(opts *Options, args []string) int { 1155 commandsByFile := make(map[string][]commandsForTarget) 1156 if opts.CommandsFile != "" { 1157 appendCommandsFromFile(opts, commandsByFile, opts.CommandsFile) 1158 } else { 1159 if len(args) == 0 { 1160 Usage() 1161 } 1162 appendCommands(opts, commandsByFile, args) 1163 } 1164 1165 numFiles := len(commandsByFile) 1166 if opts.Parallelism > 0 { 1167 runtime.GOMAXPROCS(opts.Parallelism) 1168 } 1169 results := make(chan *rewriteResult, numFiles) 1170 data := make(chan commandsForFile) 1171 1172 for i := 0; i < opts.NumIO; i++ { 1173 go func(results chan *rewriteResult, data chan commandsForFile) { 1174 for commandsForFile := range data { 1175 results <- rewrite(opts, commandsForFile) 1176 } 1177 }(results, data) 1178 } 1179 1180 for file, commands := range commandsByFile { 1181 data <- commandsForFile{file, commands} 1182 } 1183 close(data) 1184 records := []*apipb.Output_Record{} 1185 hasErrors := false 1186 for i := 0; i < numFiles; i++ { 1187 fileResults := <-results 1188 if fileResults == nil { 1189 continue 1190 } 1191 hasErrors = hasErrors || len(fileResults.errs) > 0 1192 for _, err := range fileResults.errs { 1193 fmt.Fprintf(os.Stderr, "%s: %s\n", fileResults.file, err) 1194 } 1195 if fileResults.modified && !opts.Quiet { 1196 fmt.Fprintf(os.Stderr, "fixed %s\n", fileResults.file) 1197 } 1198 if fileResults.records != nil { 1199 records = append(records, fileResults.records...) 1200 } 1201 } 1202 1203 if opts.IsPrintingProto { 1204 data, err := proto.Marshal(&apipb.Output{Records: records}) 1205 if err != nil { 1206 log.Fatal("marshaling error: ", err) 1207 } 1208 fmt.Fprintf(os.Stdout, "%s", data) 1209 } else { 1210 for _, record := range records { 1211 printRecord(os.Stdout, record) 1212 } 1213 } 1214 1215 if hasErrors { 1216 return 2 1217 } 1218 if !fileModified && !opts.Stdout { 1219 return 3 1220 } 1221 return 0 1222 }