/* Copyright 2016 Google LLC 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 https://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" "github.com/bazelbuild/buildtools/build" "github.com/bazelbuild/buildtools/buildifier/config" "github.com/bazelbuild/buildtools/buildifier/utils" "github.com/bazelbuild/buildtools/differ" "github.com/bazelbuild/buildtools/wspace" ) var buildVersion = "redacted" var buildScmRevision = "redacted" func usage() { fmt.Fprintf(flag.CommandLine.Output(), `usage: buildifier [-d] [-v] [-r] [-config=path.json] [-diff_command=command] [-help] [-multi_diff] [-mode=mode] [-lint=lint_mode] [-path=path] [files...] Buildifier applies standard formatting to the named Starlark 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. It creates the diffs by running a diff command, which can be specified using the -diff_command flag. You can indicate that the diff command can show differences between more than two files in the manner of tkdiff by specifying the -multi_diff flag. 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. The lint flag selects the lint mode to be used: off, warn, fix. In off mode, the linting is not performed. In warn mode, buildifier prints warnings for common mistakes and suboptimal coding practices that include links providing more context and fix suggestions. In fix mode, buildifier updates the files with all warning resolutions produced by automated fixes. The default lint mode is off. If no files are listed, buildifier reads a Starlark file from standard input. In fix mode, it writes the reformatted Starlark 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. Return codes used by buildifier: 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) Full list of flags with their defaults: `) flag.PrintDefaults() fmt.Fprintf(flag.CommandLine.Output(), ` Buildifier can be also be configured via a JSON file. The location of the file is given by the -config flag, the BUILDIFIER_CONFIG environment variable, or a file named '.buildifier.json' at the root of the workspace (e.g., in the same directory as the WORKSPACE file). The PWD environment variable or process working directory is used to help find the workspace root. If present, the file is loaded into memory and becomes the base configuration that command line flags override. A sample configuration file can be printed to stdout by running buildifier -config=example. The config file feature can be disabled completely with -config=off. `) } func main() { c := config.New() flags := c.FlagSet("buildifier", flag.ExitOnError) flag.CommandLine = flags flag.Usage = usage flags.Parse(os.Args[1:]) args := flags.Args() if c.Help { flag.CommandLine.SetOutput(os.Stdout) usage() fmt.Println() os.Exit(0) } if c.Version { fmt.Printf("buildifier version: %s \n", buildVersion) fmt.Printf("buildifier scm revision: %s \n", buildScmRevision) os.Exit(0) } if c.ConfigPath == "" { c.ConfigPath = config.FindConfigPath("") } if c.ConfigPath != "" { if c.ConfigPath == "example" { fmt.Println(config.Example().String()) os.Exit(0) } if c.ConfigPath != "off" { if err := c.LoadFile(); err != nil { fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) os.Exit(2) } // re-parse with new possibly new defaults flags = c.FlagSet("buildifier", flag.ExitOnError) flag.CommandLine = flags flag.Usage = usage flags.Parse(os.Args[1:]) } } if err := c.Validate(args); err != nil { fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) os.Exit(2) } // Pass down debug flags into build package build.DisableRewrites = c.DisableRewrites build.AllowSort = c.AllowSort differ, deprecationWarning := differ.Find() if c.DiffCommand != "" { differ.Cmd = c.DiffCommand differ.MultiDiff = c.MultiDiff } else { if deprecationWarning && c.Mode == "diff" { fmt.Fprintf(os.Stderr, "buildifier: selecting diff program with the BUILDIFIER_DIFF, BUILDIFIER_MULTIDIFF, and DISPLAY environment variables is deprecated, use flags -diff_command and -multi_diff instead\n") } } b := buildifier{c, differ} exitCode := b.run(args) os.Exit(exitCode) } type buildifier struct { config *config.Config differ *differ.Differ } func (b *buildifier) run(args []string) int { tf := &utils.TempFile{} defer tf.Clean() exitCode := 0 var diagnostics *utils.Diagnostics if len(args) == 0 || (len(args) == 1 && (args)[0] == "-") { // 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) return 2 } if b.config.Mode == "fix" { b.config.Mode = "pipe" } var fileDiagnostics *utils.FileDiagnostics fileDiagnostics, exitCode = b.processFile("", data, false, tf) diagnostics = utils.NewDiagnostics(fileDiagnostics) } else { files := args if b.config.Recursive { var err error files, err = utils.ExpandDirectories(&args) if err != nil { fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) return 3 } } diagnostics, exitCode = b.processFiles(files, tf) } diagnosticsOutput := diagnostics.Format(b.config.Format, b.config.Verbose) if b.config.Format != "" { // Explicitly provided --format means the diagnostics are printed to stdout fmt.Print(diagnosticsOutput) // Exit code should be set to 0 so that other tools know they can safely parse the json exitCode = 0 } else { // --format is not provided, stdout is reserved for file contents fmt.Fprint(os.Stderr, diagnosticsOutput) } if err := b.differ.Run(); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return 2 } return exitCode } func (b *buildifier) processFiles(files []string, tf *utils.TempFile) (*utils.Diagnostics, int) { // 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) } exitCode := 0 fileDiagnostics := []*utils.FileDiagnostics{} // 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 } fd, newExitCode := b.processFile(file, res.data, len(files) > 1, tf) if fd != nil { fileDiagnostics = append(fileDiagnostics, fd) } if newExitCode != 0 { exitCode = newExitCode } } return utils.NewDiagnostics(fileDiagnostics...), exitCode } // processFile processes a single file containing data. // It has been read from filename and should be written back if fixing. func (b *buildifier) processFile(filename string, data []byte, displayFileNames bool, tf *utils.TempFile) (*utils.FileDiagnostics, int) { var exitCode int displayFilename := filename if b.config.WorkspaceRelativePath != "" { displayFilename = b.config.WorkspaceRelativePath } parser := utils.GetParser(b.config.InputType) f, err := parser(displayFilename, 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 utils.InvalidFileDiagnostics(displayFilename), exitCode } if absoluteFilename, err := filepath.Abs(displayFilename); err == nil { f.WorkspaceRoot, f.Pkg, f.Label = wspace.SplitFilePath(absoluteFilename) } warnings := utils.Lint(f, b.config.Lint, &b.config.LintWarnings, b.config.Verbose) if len(warnings) > 0 { exitCode = 4 } fileDiagnostics := utils.NewFileDiagnostics(f.DisplayPath(), warnings) ndata := build.Format(f) switch b.config.Mode { case "check": // check mode: print names of files that need formatting. if !bytes.Equal(data, ndata) { fileDiagnostics.Formatted = false return fileDiagnostics, 4 } case "diff": // diff mode: run diff on old and new. if bytes.Equal(data, ndata) { return fileDiagnostics, exitCode } outfile, err := tf.WriteTemp(ndata) if err != nil { fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) return fileDiagnostics, 3 } infile := filename if filename == "" { // data was read from standard filename. // Write it to a temporary file so diff can read it. infile, err = tf.WriteTemp(data) if err != nil { fmt.Fprintf(os.Stderr, "buildifier: %v\n", err) return fileDiagnostics, 3 } } if displayFileNames { fmt.Fprintf(os.Stderr, "%v:\n", f.DisplayPath()) } if err := b.differ.Show(infile, outfile); err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) return fileDiagnostics, 4 } 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) case "fix": // fix mode: update files in place as needed. if bytes.Equal(data, ndata) { return fileDiagnostics, exitCode } err := ioutil.WriteFile(filename, ndata, 0666) if err != nil { fmt.Fprintf(os.Stderr, "buildifier: %s\n", err) return fileDiagnostics, 3 } if b.config.Verbose { fmt.Fprintf(os.Stderr, "fixed %s\n", f.DisplayPath()) } case "print_if_changed": if bytes.Equal(data, ndata) { return fileDiagnostics, exitCode } if _, err := os.Stdout.Write(ndata); err != nil { fmt.Fprintf(os.Stderr, "buildifier: error writing output: %v\n", err) return fileDiagnostics, 3 } } return fileDiagnostics, exitCode }