blob: 413949264400907b8a513021eee45dcf3ea39721 [file] [log] [blame] [edit]
/*
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.
*/
// Buildifier, a tool to parse and format BUILD files.
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"github.com/bazelbuild/buildtools/build"
"github.com/bazelbuild/buildtools/differ"
"github.com/bazelbuild/buildtools/tables"
)
var buildifierVersion = "redacted"
var buildScmRevision = "redacted"
var (
// Undocumented; for debugging.
showlog = flag.Bool("showlog", false, "show log in check mode")
vflag = flag.Bool("v", false, "print verbose information on standard error")
dflag = flag.Bool("d", false, "alias for -mode=diff")
mode = flag.String("mode", "", "formatting mode: check, diff, or fix (default fix)")
path = flag.String("path", "", "assume BUILD file has this path relative to the workspace directory")
tablesPath = flag.String("tables", "", "path to JSON file with custom table definitions which will replace the built-in tables")
addTablesPath = flag.String("add_tables", "", "path to JSON file with custom table definitions which will be merged with the built-in tables")
version = flag.Bool("version", false, "Print the version of buildifier")
// Debug flags passed through to rewrite.go
allowSort = stringList("allowsort", "additional sort contexts to treat as safe")
disable = stringList("buildifier_disable", "list of buildifier rewrites to disable")
// Experimental flags
inputType = flag.String("type", "build", "Experimental: input file type: build (default, for BUILD files) or bzl (for .bzl files).")
)
func stringList(name, help string) func() []string {
f := flag.String(name, "", help)
return func() []string {
return strings.Split(*f, ",")
}
}
func usage() {
fmt.Fprintf(os.Stderr, `usage: buildifier [-d] [-v] [-mode=mode] [-path=path] [files...]
Buildifier applies a standard formatting to the named BUILD files.
The mode flag selects the processing: check, diff, fix, or print_if_changed.
In check mode, buildifier prints a list of files that need reformatting.
In diff mode, buildifier shows the diffs that it would make.
In fix mode, buildifier updates the files that need reformatting and,
if the -v flag is given, prints their names to standard error.
In print_if_changed mode, buildifier shows the file contents it would write.
The default mode is fix. -d is an alias for -mode=diff.
If no files are listed, buildifier reads a BUILD file from standard input. In
fix mode, it writes the reformatted BUILD file to standard output, even if no
changes are necessary.
Buildifier's reformatting depends in part on the path to the file relative
to the workspace directory. Normally buildifier deduces that path from the
file names given, but the path can be given explicitly with the -path
argument. This is especially useful when reformatting standard input,
or in scripts that reformat a temporary copy of a file.
`)
os.Exit(2)
}
func main() {
flag.Usage = usage
flag.Parse()
args := flag.Args()
if *version {
fmt.Printf("buildifier version: %s \n", buildifierVersion)
fmt.Printf("buildifier scm revision: %s \n", buildScmRevision)
if len(args) == 0 {
os.Exit(0)
}
}
// Pass down debug flags into build package
build.DisableRewrites = disable()
build.AllowSort = allowSort()
// Check input type.
switch *inputType {
case "build", "bzl", "auto":
// ok
default:
fmt.Fprintf(os.Stderr, "buildifier: unrecognized input type %s; valid types are build, bzl, auto\n", *inputType)
os.Exit(2)
}
if *dflag {
if *mode != "" {
fmt.Fprintf(os.Stderr, "buildifier: cannot specify both -d and -mode flags\n")
os.Exit(2)
}
*mode = "diff"
}
// Check mode.
switch *mode {
case "":
*mode = "fix"
case "check", "diff", "fix", "print_if_changed":
// ok
default:
fmt.Fprintf(os.Stderr, "buildifier: unrecognized mode %s; valid modes are check, diff, fix\n", *mode)
os.Exit(2)
}
// If the path flag is set, must only be formatting a single file.
// It doesn't make sense for multiple files to have the same path.
if (*path != "" || *mode == "print_if_changed") && len(args) > 1 {
fmt.Fprintf(os.Stderr, "buildifier: can only format one file when using -path flag or -mode=print_if_changed\n")
os.Exit(2)
}
if *tablesPath != "" {
if err := tables.ParseAndUpdateJSONDefinitions(*tablesPath, false); err != nil {
fmt.Fprintf(os.Stderr, "buildifier: failed to parse %s for -tables: %s\n", *tablesPath, err)
os.Exit(2)
}
}
if *addTablesPath != "" {
if err := tables.ParseAndUpdateJSONDefinitions(*addTablesPath, true); err != nil {
fmt.Fprintf(os.Stderr, "buildifier: failed to parse %s for -add_tables: %s\n", *addTablesPath, err)
os.Exit(2)
}
}
diff = differ.Find()
if len(args) == 0 || (len(args) == 1 && args[0] == "-") {
if *inputType == "auto" {
fmt.Fprintf(os.Stderr, "buildifier: --type=auto can't be used with stdin\n")
os.Exit(2)
}
// Read from stdin, write to stdout.
data, err := ioutil.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stderr, "buildifier: reading stdin: %v\n", err)
os.Exit(2)
}
if *mode == "fix" {
*mode = "pipe"
}
processFile("stdin", data, *inputType)
} else {
processFiles(args, *inputType)
}
diff.Run()
for _, file := range toRemove {
os.Remove(file)
}
os.Exit(exitCode)
}
func processFiles(files []string, inputType string) {
// Decide how many file reads to run in parallel.
// At most 100, and at most one per 10 input files.
nworker := 100
if n := (len(files) + 9) / 10; nworker > n {
nworker = n
}
runtime.GOMAXPROCS(nworker + 1)
// Start nworker workers reading stripes of the input
// argument list and sending the resulting data on
// separate channels. file[k] is read by worker k%nworker
// and delivered on ch[k%nworker].
type result struct {
file string
data []byte
err error
}
ch := make([]chan result, nworker)
for i := 0; i < nworker; i++ {
ch[i] = make(chan result, 1)
go func(i int) {
for j := i; j < len(files); j += nworker {
file := files[j]
data, err := ioutil.ReadFile(file)
ch[i] <- result{file, data, err}
}
}(i)
}
// Process files. The processing still runs in a single goroutine
// in sequence. Only the reading of the files has been parallelized.
// The goal is to optimize for runs where most files are already
// formatted correctly, so that reading is the bulk of the I/O.
for i, file := range files {
res := <-ch[i%nworker]
if res.file != file {
fmt.Fprintf(os.Stderr, "buildifier: internal phase error: got %s for %s", res.file, file)
os.Exit(3)
}
if res.err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %v\n", res.err)
exitCode = 3
continue
}
processFile(file, res.data, inputType)
}
}
// exitCode is the code to use when exiting the program.
// The codes used by buildifier are:
//
// 0: success, everything went well
// 1: syntax errors in input
// 2: usage errors: invoked incorrectly
// 3: unexpected runtime errors: file I/O problems or internal bugs
// 4: check mode failed (reformat is needed)
var exitCode = 0
// toRemove is a list of files to remove before exiting.
var toRemove []string
// diff is the differ to use when *mode == "diff".
var diff *differ.Differ
// processFile processes a single file containing data.
// It has been read from filename and should be written back if fixing.
func processFile(filename string, data []byte, inputType string) {
defer func() {
if err := recover(); err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %s: internal error: %v\n", filename, err)
exitCode = 3
}
}()
defer setFormattingMode(inputType, filename)()
f, err := build.Parse(filename, data)
if err != nil {
// Do not use buildifier: prefix on this error.
// Since it is a parse error, it begins with file:line:
// and we want that to be the first thing in the error.
fmt.Fprintf(os.Stderr, "%v\n", err)
if exitCode < 1 {
exitCode = 1
}
return
}
if *path != "" {
f.Path = *path
}
beforeRewrite := build.Format(f)
var info build.RewriteInfo
if tables.FormattingMode == tables.BuildMode {
build.Rewrite(f, &info)
}
ndata := build.Format(f)
switch *mode {
case "check":
// check mode: print names of files that need formatting.
if !bytes.Equal(data, ndata) {
// Print:
// name # list of what changed
reformat := ""
if !bytes.Equal(data, beforeRewrite) {
reformat = " reformat"
}
log := ""
if len(info.Log) > 0 && *showlog {
sort.Strings(info.Log)
var uniq []string
last := ""
for _, s := range info.Log {
if s != last {
last = s
uniq = append(uniq, s)
}
}
log = " " + strings.Join(uniq, " ")
}
fmt.Printf("%s #%s %s%s\n", filename, reformat, &info, log)
exitCode = 4
}
return
case "diff":
// diff mode: run diff on old and new.
if bytes.Equal(data, ndata) {
return
}
outfile, err := writeTemp(ndata)
if err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %v\n", err)
exitCode = 3
return
}
infile := filename
if filename == "" {
// data was read from standard filename.
// Write it to a temporary file so diff can read it.
infile, err = writeTemp(data)
if err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %v\n", err)
exitCode = 3
return
}
}
diff.Show(infile, outfile)
case "pipe":
// pipe mode - reading from stdin, writing to stdout.
// ("pipe" is not from the command line; it is set above in main.)
os.Stdout.Write(ndata)
return
case "fix":
// fix mode: update files in place as needed.
if bytes.Equal(data, ndata) {
return
}
err := ioutil.WriteFile(filename, ndata, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "buildifier: %s\n", err)
exitCode = 3
return
}
if *vflag {
fmt.Fprintf(os.Stderr, "fixed %s\n", filename)
}
case "print_if_changed":
if bytes.Equal(data, ndata) {
return
}
if _, err := os.Stdout.Write(ndata); err != nil {
fmt.Fprintf(os.Stderr, "buildifier: error writing output: %v\n", err)
exitCode = 3
return
}
}
}
func setFormattingMode(inputType, filename string) func() {
defaultMode := tables.FormattingMode
switch inputType {
case "build":
tables.FormattingMode = tables.BuildMode
case "bzl":
tables.FormattingMode = tables.DefaultMode
case "auto":
switch base := filepath.Base(filename); base {
case "BUILD", "BUILD.bazel", "WORKSPACE":
tables.FormattingMode = tables.BuildMode
default:
tables.FormattingMode = tables.DefaultMode
}
}
return func() {
tables.FormattingMode = defaultMode
}
}
// writeTemp writes data to a temporary file and returns the name of the file.
func writeTemp(data []byte) (file string, err error) {
f, err := ioutil.TempFile("", "buildifier-tmp-")
if err != nil {
return "", fmt.Errorf("creating temporary file: %v", err)
}
name := f.Name()
toRemove = append(toRemove, name)
defer f.Close()
_, err = f.Write(data)
if err != nil {
return "", fmt.Errorf("writing temporary file: %v", err)
}
return name, nil
}