github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/cmd/gazelle/fix-update.go (about)

     1  /* Copyright 2017 The Bazel Authors. All rights reserved.
     2  
     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  
     7     http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package main
    17  
    18  import (
    19  	"errors"
    20  	"flag"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"log"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/bazelbuild/bazel-gazelle/internal/config"
    29  	"github.com/bazelbuild/bazel-gazelle/internal/label"
    30  	"github.com/bazelbuild/bazel-gazelle/internal/merger"
    31  	"github.com/bazelbuild/bazel-gazelle/internal/packages"
    32  	"github.com/bazelbuild/bazel-gazelle/internal/repos"
    33  	"github.com/bazelbuild/bazel-gazelle/internal/resolve"
    34  	"github.com/bazelbuild/bazel-gazelle/internal/rules"
    35  	"github.com/bazelbuild/bazel-gazelle/internal/wspace"
    36  	bf "github.com/bazelbuild/buildtools/build"
    37  )
    38  
    39  // updateConfig holds configuration information needed to run the fix and
    40  // update commands. This includes everything in config.Config, but it also
    41  // includes some additional fields that aren't relevant to other packages.
    42  type updateConfig struct {
    43  	c                 *config.Config
    44  	emit              emitFunc
    45  	outDir, outSuffix string
    46  	repos             []repos.Repo
    47  }
    48  
    49  type emitFunc func(*config.Config, *bf.File, string) error
    50  
    51  var modeFromName = map[string]emitFunc{
    52  	"print": printFile,
    53  	"fix":   fixFile,
    54  	"diff":  diffFile,
    55  }
    56  
    57  // visitRecord stores information about about a directory visited with
    58  // packages.Walk.
    59  type visitRecord struct {
    60  	// pkgRel is the slash-separated path to the visited directory, relative to
    61  	// the repository root. "" for the repository root itself.
    62  	pkgRel string
    63  
    64  	// rules is a list of generated Go rules.
    65  	rules []bf.Expr
    66  
    67  	// empty is a list of empty Go rules that may be deleted.
    68  	empty []bf.Expr
    69  
    70  	// file is the build file being processed.
    71  	file *bf.File
    72  }
    73  
    74  type byPkgRel []visitRecord
    75  
    76  func (vs byPkgRel) Len() int           { return len(vs) }
    77  func (vs byPkgRel) Less(i, j int) bool { return vs[i].pkgRel < vs[j].pkgRel }
    78  func (vs byPkgRel) Swap(i, j int)      { vs[i], vs[j] = vs[j], vs[i] }
    79  
    80  func runFixUpdate(cmd command, args []string) error {
    81  	uc, err := newFixUpdateConfiguration(cmd, args)
    82  	if err != nil {
    83  		return err
    84  	}
    85  	if cmd == fixCmd {
    86  		// Only check the version when "fix" is run. Generated build files
    87  		// frequently work with older version of rules_go, and we don't want to
    88  		// nag too much since there's no way to disable this warning.
    89  		checkRulesGoVersion(uc.c.RepoRoot)
    90  	}
    91  
    92  	l := label.NewLabeler(uc.c)
    93  	ruleIndex := resolve.NewRuleIndex()
    94  
    95  	var visits []visitRecord
    96  
    97  	// Visit all directories in the repository.
    98  	packages.Walk(uc.c, uc.c.RepoRoot, func(dir, rel string, c *config.Config, pkg *packages.Package, file *bf.File, isUpdateDir bool) {
    99  		// If this file is ignored or if Gazelle was not asked to update this
   100  		// directory, just index the build file and move on.
   101  		if !isUpdateDir {
   102  			if file != nil {
   103  				ruleIndex.AddRulesFromFile(c, file)
   104  			}
   105  			return
   106  		}
   107  
   108  		// Fix any problems in the file.
   109  		if file != nil {
   110  			file = merger.FixFileMinor(c, file)
   111  			fixedFile := merger.FixFile(c, file)
   112  			if cmd == fixCmd {
   113  				file = fixedFile
   114  			} else if fixedFile != file {
   115  				log.Printf("%s: warning: file contains rules whose structure is out of date. Consider running 'gazelle fix'.", file.Path)
   116  			}
   117  		}
   118  
   119  		// If the file exists, but no Go code is present, create an empty package.
   120  		// This lets us delete existing rules.
   121  		if pkg == nil && file != nil {
   122  			pkg = packages.EmptyPackage(c, dir, rel)
   123  		}
   124  
   125  		// Generate new rules and merge them into the existing file (if present).
   126  		if pkg != nil {
   127  			g := rules.NewGenerator(c, l, file)
   128  			rules, empty, err := g.GenerateRules(pkg)
   129  			if err != nil {
   130  				log.Print(err)
   131  				return
   132  			}
   133  			if file == nil {
   134  				file = &bf.File{
   135  					Path: filepath.Join(c.RepoRoot, filepath.FromSlash(rel), c.DefaultBuildFileName()),
   136  					Stmt: rules,
   137  				}
   138  			} else {
   139  				file, rules = merger.MergeFile(rules, empty, file, merger.PreResolveAttrs)
   140  			}
   141  			visits = append(visits, visitRecord{
   142  				pkgRel: rel,
   143  				rules:  rules,
   144  				empty:  empty,
   145  				file:   file,
   146  			})
   147  		}
   148  
   149  		// Add library rules to the dependency resolution table.
   150  		if file != nil {
   151  			ruleIndex.AddRulesFromFile(c, file)
   152  		}
   153  	})
   154  
   155  	// Finish building the index for dependency resolution.
   156  	ruleIndex.Finish()
   157  
   158  	// Resolve dependencies.
   159  	rc := repos.NewRemoteCache(uc.repos)
   160  	resolver := resolve.NewResolver(uc.c, l, ruleIndex, rc)
   161  	for i := range visits {
   162  		for j := range visits[i].rules {
   163  			visits[i].rules[j] = resolver.ResolveRule(visits[i].rules[j], visits[i].pkgRel)
   164  		}
   165  		visits[i].file, _ = merger.MergeFile(visits[i].rules, visits[i].empty, visits[i].file, merger.PostResolveAttrs)
   166  	}
   167  
   168  	// Emit merged files.
   169  	for _, v := range visits {
   170  		rules.SortLabels(v.file)
   171  		v.file = merger.FixLoads(v.file)
   172  		bf.Rewrite(v.file, nil) // have buildifier 'format' our rules.
   173  
   174  		path := v.file.Path
   175  		if uc.outDir != "" {
   176  			stem := filepath.Base(v.file.Path) + uc.outSuffix
   177  			path = filepath.Join(uc.outDir, v.pkgRel, stem)
   178  		}
   179  		if err := uc.emit(uc.c, v.file, path); err != nil {
   180  			log.Print(err)
   181  		}
   182  	}
   183  	return nil
   184  }
   185  
   186  func newFixUpdateConfiguration(cmd command, args []string) (*updateConfig, error) {
   187  	uc := &updateConfig{c: &config.Config{}}
   188  	var err error
   189  
   190  	fs := flag.NewFlagSet("gazelle", flag.ContinueOnError)
   191  	// Flag will call this on any parse error. Don't print usage unless
   192  	// -h or -help were passed explicitly.
   193  	fs.Usage = func() {}
   194  
   195  	knownImports := multiFlag{}
   196  	buildFileName := fs.String("build_file_name", "BUILD.bazel,BUILD", "comma-separated list of valid build file names.\nThe first element of the list is the name of output build files to generate.")
   197  	buildTags := fs.String("build_tags", "", "comma-separated list of build tags. If not specified, Gazelle will not\n\tfilter sources with build constraints.")
   198  	external := fs.String("external", "external", "external: resolve external packages with go_repository\n\tvendored: resolve external packages as packages in vendor/")
   199  	var goPrefix explicitFlag
   200  	fs.Var(&goPrefix, "go_prefix", "prefix of import paths in the current workspace")
   201  	repoRoot := fs.String("repo_root", "", "path to a directory which corresponds to go_prefix, otherwise gazelle searches for it.")
   202  	fs.Var(&knownImports, "known_import", "import path for which external resolution is skipped (can specify multiple times)")
   203  	mode := fs.String("mode", "fix", "print: prints all of the updated BUILD files\n\tfix: rewrites all of the BUILD files in place\n\tdiff: computes the rewrite but then just does a diff")
   204  	outDir := fs.String("experimental_out_dir", "", "write build files to an alternate directory tree")
   205  	outSuffix := fs.String("experimental_out_suffix", "", "extra suffix appended to build file names. Only used if -experimental_out_dir is also set.")
   206  	var proto explicitFlag
   207  	fs.Var(&proto, "proto", "default: generates new proto rules\n\tdisable: does not touch proto rules\n\tlegacy (deprecated): generates old proto rules")
   208  	if err := fs.Parse(args); err != nil {
   209  		if err == flag.ErrHelp {
   210  			fixUpdateUsage(fs)
   211  			os.Exit(0)
   212  		}
   213  		// flag already prints the error; don't print it again.
   214  		log.Fatal("Try -help for more information.")
   215  	}
   216  
   217  	uc.c.Dirs = fs.Args()
   218  	if len(uc.c.Dirs) == 0 {
   219  		uc.c.Dirs = []string{"."}
   220  	}
   221  	for i := range uc.c.Dirs {
   222  		uc.c.Dirs[i], err = filepath.Abs(uc.c.Dirs[i])
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  	}
   227  
   228  	if *repoRoot != "" {
   229  		uc.c.RepoRoot = *repoRoot
   230  	} else if len(uc.c.Dirs) == 1 {
   231  		uc.c.RepoRoot, err = wspace.Find(uc.c.Dirs[0])
   232  		if err != nil {
   233  			return nil, fmt.Errorf("-repo_root not specified, and WORKSPACE cannot be found: %v", err)
   234  		}
   235  	} else {
   236  		uc.c.RepoRoot, err = wspace.Find(".")
   237  		if err != nil {
   238  			return nil, fmt.Errorf("-repo_root not specified, and WORKSPACE cannot be found: %v", err)
   239  		}
   240  	}
   241  
   242  	for _, dir := range uc.c.Dirs {
   243  		if !isDescendingDir(dir, uc.c.RepoRoot) {
   244  			return nil, fmt.Errorf("dir %q is not a subdirectory of repo root %q", dir, uc.c.RepoRoot)
   245  		}
   246  	}
   247  
   248  	uc.c.ValidBuildFileNames = strings.Split(*buildFileName, ",")
   249  	if len(uc.c.ValidBuildFileNames) == 0 {
   250  		return nil, fmt.Errorf("no valid build file names specified")
   251  	}
   252  
   253  	uc.c.SetBuildTags(*buildTags)
   254  	uc.c.PreprocessTags()
   255  
   256  	if goPrefix.set {
   257  		uc.c.GoPrefix = goPrefix.value
   258  	} else {
   259  		uc.c.GoPrefix, err = loadGoPrefix(uc.c)
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  	if err := config.CheckPrefix(uc.c.GoPrefix); err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	uc.c.ShouldFix = cmd == fixCmd
   269  
   270  	uc.c.DepMode, err = config.DependencyModeFromString(*external)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  
   275  	if proto.set {
   276  		uc.c.ProtoMode, err = config.ProtoModeFromString(proto.value)
   277  		if err != nil {
   278  			return nil, err
   279  		}
   280  		uc.c.ProtoModeExplicit = true
   281  	}
   282  
   283  	emit, ok := modeFromName[*mode]
   284  	if !ok {
   285  		return nil, fmt.Errorf("unrecognized emit mode: %q", *mode)
   286  	}
   287  	uc.emit = emit
   288  
   289  	uc.outDir = *outDir
   290  	uc.outSuffix = *outSuffix
   291  
   292  	workspacePath := filepath.Join(uc.c.RepoRoot, "WORKSPACE")
   293  	workspaceContent, err := ioutil.ReadFile(workspacePath)
   294  	if os.IsNotExist(err) {
   295  		workspaceContent = nil
   296  	} else if err != nil {
   297  		return nil, err
   298  	}
   299  	workspace, err := bf.Parse(workspacePath, workspaceContent)
   300  	if err != nil {
   301  		return nil, err
   302  	}
   303  	uc.repos = repos.ListRepositories(workspace)
   304  	repoPrefixes := make(map[string]bool)
   305  	for _, r := range uc.repos {
   306  		repoPrefixes[r.GoPrefix] = true
   307  	}
   308  	for _, imp := range knownImports {
   309  		if repoPrefixes[imp] {
   310  			continue
   311  		}
   312  		repo := repos.Repo{
   313  			Name:     label.ImportPathToBazelRepoName(imp),
   314  			GoPrefix: imp,
   315  		}
   316  		uc.repos = append(uc.repos, repo)
   317  	}
   318  
   319  	return uc, nil
   320  }
   321  
   322  func fixUpdateUsage(fs *flag.FlagSet) {
   323  	fmt.Fprintln(os.Stderr, `usage: gazelle [fix|update] [flags...] [package-dirs...]
   324  
   325  The update command creates new build files and update existing BUILD files
   326  when needed.
   327  
   328  The fix command also creates and updates build files, and in addition, it may
   329  make potentially breaking updates to usage of rules. For example, it may
   330  delete obsolete rules or rename existing rules.
   331  
   332  There are several output modes which can be selected with the -mode flag. The
   333  output mode determines what Gazelle does with updated BUILD files.
   334  
   335    fix (default) - write updated BUILD files back to disk.
   336    print - print updated BUILD files to stdout.
   337    diff - diff updated BUILD files against existing files in unified format.
   338  
   339  Gazelle accepts a list of paths to Go package directories to process (defaults
   340  to the working directory if none are given). It recursively traverses
   341  subdirectories. All directories must be under the directory specified by
   342  -repo_root; if -repo_root is not given, this is the directory containing the
   343  WORKSPACE file.
   344  
   345  FLAGS:
   346  `)
   347  	fs.PrintDefaults()
   348  }
   349  
   350  func loadBuildFile(c *config.Config, dir string) (*bf.File, error) {
   351  	var buildPath string
   352  	for _, base := range c.ValidBuildFileNames {
   353  		p := filepath.Join(dir, base)
   354  		fi, err := os.Stat(p)
   355  		if err == nil {
   356  			if fi.Mode().IsRegular() {
   357  				buildPath = p
   358  				break
   359  			}
   360  			continue
   361  		}
   362  		if !os.IsNotExist(err) {
   363  			return nil, err
   364  		}
   365  	}
   366  	if buildPath == "" {
   367  		return nil, os.ErrNotExist
   368  	}
   369  
   370  	data, err := ioutil.ReadFile(buildPath)
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  	return bf.Parse(buildPath, data)
   375  }
   376  
   377  func loadGoPrefix(c *config.Config) (string, error) {
   378  	f, err := loadBuildFile(c, c.RepoRoot)
   379  	if err != nil {
   380  		return "", errors.New("-go_prefix not set")
   381  	}
   382  	for _, d := range config.ParseDirectives(f) {
   383  		if d.Key == "prefix" {
   384  			return d.Value, nil
   385  		}
   386  	}
   387  	for _, s := range f.Stmt {
   388  		c, ok := s.(*bf.CallExpr)
   389  		if !ok {
   390  			continue
   391  		}
   392  		l, ok := c.X.(*bf.LiteralExpr)
   393  		if !ok {
   394  			continue
   395  		}
   396  		if l.Token != "go_prefix" {
   397  			continue
   398  		}
   399  		if len(c.List) != 1 {
   400  			return "", fmt.Errorf("-go_prefix not set, and %s has go_prefix(%v) with too many args", f.Path, c.List)
   401  		}
   402  		v, ok := c.List[0].(*bf.StringExpr)
   403  		if !ok {
   404  			return "", fmt.Errorf("-go_prefix not set, and %s has go_prefix(%v) which is not a string", f.Path, bf.FormatString(c.List[0]))
   405  		}
   406  		return v.Value, nil
   407  	}
   408  	return "", fmt.Errorf("-go_prefix not set, and no # gazelle:prefix directive found in %s", f.Path)
   409  }
   410  
   411  func isDescendingDir(dir, root string) bool {
   412  	rel, err := filepath.Rel(root, dir)
   413  	if err != nil {
   414  		return false
   415  	}
   416  	if rel == "." {
   417  		return true
   418  	}
   419  	return !strings.HasPrefix(rel, "..")
   420  }