vendor buildozer
This commit is contained in:
976
vendor/github.com/bazelbuild/buildtools/edit/buildozer.go
generated
vendored
Normal file
976
vendor/github.com/bazelbuild/buildtools/edit/buildozer.go
generated
vendored
Normal file
@@ -0,0 +1,976 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
// Opts represents the options to be used by buildozer, and can be overriden before calling Buildozer.
|
||||
var Opts = 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(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(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{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(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{
|
||||
&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{
|
||||
&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{
|
||||
&apipb.Output_Record_Field{Value: &apipb.Output_Record_Field_Text{commentsText(comments)}},
|
||||
}
|
||||
default:
|
||||
panic("cmdPrintComment")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func cmdDelete(env CmdEnvironment) (*build.File, error) {
|
||||
return DeleteRule(env.File, env.Rule), nil
|
||||
}
|
||||
|
||||
func cmdMove(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(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: 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(env CmdEnvironment) (*build.File, error) {
|
||||
env.File.Stmt = InsertLoad(env.File.Stmt, env.Args)
|
||||
return env.File, nil
|
||||
}
|
||||
|
||||
func cmdPrint(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 == "label" {
|
||||
if env.Rule.Attr("name") != nil {
|
||||
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(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(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(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 cmdSet(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(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(env CmdEnvironment) (*build.File, error) {
|
||||
attrName := env.Args[0]
|
||||
from := env.Args[1]
|
||||
|
||||
return copyAttributeBetweenRules(env, attrName, from)
|
||||
}
|
||||
|
||||
func cmdCopyNoOverwrite(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(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(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>"},
|
||||
"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(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(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(targets)
|
||||
for _, cmd := range commands {
|
||||
for _, r := range targets {
|
||||
cmdInfo := AllCommands[cmd.tokens[0]]
|
||||
record := &apipb.Output_Record{}
|
||||
newf, err := cmdInfo.Fn(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(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(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(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(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(target)
|
||||
}
|
||||
|
||||
for _, file := range buildFiles {
|
||||
commandMap[file] = append(commandMap[file], commandsForTarget{target, commands})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendCommandsFromFile(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(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(args []string) int {
|
||||
commandsByFile := make(map[string][]commandsForTarget)
|
||||
if Opts.CommandsFile != "" {
|
||||
appendCommandsFromFile(commandsByFile, Opts.CommandsFile)
|
||||
} else {
|
||||
if len(args) == 0 {
|
||||
Usage()
|
||||
}
|
||||
appendCommands(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(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
|
||||
}
|
||||
Reference in New Issue
Block a user