kubernetes/vendor/github.com/bazelbuild/buildtools/edit/buildozer.go
2019-02-12 18:38:36 -08:00

977 lines
30 KiB
Go

/*
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
}