github.com/wolfd/bazel-gazelle@v0.14.0/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  	"flag"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"log"
    23  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  
    27  	"github.com/bazelbuild/bazel-gazelle/internal/config"
    28  	gzflag "github.com/bazelbuild/bazel-gazelle/internal/flag"
    29  	"github.com/bazelbuild/bazel-gazelle/internal/label"
    30  	"github.com/bazelbuild/bazel-gazelle/internal/merger"
    31  	"github.com/bazelbuild/bazel-gazelle/internal/repos"
    32  	"github.com/bazelbuild/bazel-gazelle/internal/resolve"
    33  	"github.com/bazelbuild/bazel-gazelle/internal/rule"
    34  	"github.com/bazelbuild/bazel-gazelle/internal/walk"
    35  )
    36  
    37  // updateConfig holds configuration information needed to run the fix and
    38  // update commands. This includes everything in config.Config, but it also
    39  // includes some additional fields that aren't relevant to other packages.
    40  type updateConfig struct {
    41  	emit  emitFunc
    42  	repos []repos.Repo
    43  }
    44  
    45  type emitFunc func(path string, data []byte) error
    46  
    47  var modeFromName = map[string]emitFunc{
    48  	"print": printFile,
    49  	"fix":   fixFile,
    50  	"diff":  diffFile,
    51  }
    52  
    53  const updateName = "_update"
    54  
    55  func getUpdateConfig(c *config.Config) *updateConfig {
    56  	return c.Exts[updateName].(*updateConfig)
    57  }
    58  
    59  type updateConfigurer struct {
    60  	mode string
    61  }
    62  
    63  func (ucr *updateConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {
    64  	uc := &updateConfig{}
    65  	c.Exts[updateName] = uc
    66  
    67  	c.ShouldFix = cmd == "fix"
    68  
    69  	fs.StringVar(&ucr.mode, "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")
    70  }
    71  
    72  func (ucr *updateConfigurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
    73  	uc := getUpdateConfig(c)
    74  	var ok bool
    75  	uc.emit, ok = modeFromName[ucr.mode]
    76  	if !ok {
    77  		return fmt.Errorf("unrecognized emit mode: %q", ucr.mode)
    78  	}
    79  
    80  	c.Dirs = fs.Args()
    81  	if len(c.Dirs) == 0 {
    82  		c.Dirs = []string{"."}
    83  	}
    84  	for i := range c.Dirs {
    85  		dir, err := filepath.Abs(c.Dirs[i])
    86  		if err != nil {
    87  			return fmt.Errorf("%s: failed to find absolute path: %v", c.Dirs[i], err)
    88  		}
    89  		dir, err = filepath.EvalSymlinks(dir)
    90  		if err != nil {
    91  			return fmt.Errorf("%s: failed to resolve symlinks: %v", c.Dirs[i], err)
    92  		}
    93  		if !isDescendingDir(dir, c.RepoRoot) {
    94  			return fmt.Errorf("dir %q is not a subdirectory of repo root %q", dir, c.RepoRoot)
    95  		}
    96  		c.Dirs[i] = dir
    97  	}
    98  
    99  	return nil
   100  }
   101  
   102  func (ucr *updateConfigurer) KnownDirectives() []string { return nil }
   103  
   104  func (ucr *updateConfigurer) Configure(c *config.Config, rel string, f *rule.File) {}
   105  
   106  // visitRecord stores information about about a directory visited with
   107  // packages.Walk.
   108  type visitRecord struct {
   109  	// pkgRel is the slash-separated path to the visited directory, relative to
   110  	// the repository root. "" for the repository root itself.
   111  	pkgRel string
   112  
   113  	// rules is a list of generated Go rules.
   114  	rules []*rule.Rule
   115  
   116  	// empty is a list of empty Go rules that may be deleted.
   117  	empty []*rule.Rule
   118  
   119  	// file is the build file being processed.
   120  	file *rule.File
   121  }
   122  
   123  type byPkgRel []visitRecord
   124  
   125  func (vs byPkgRel) Len() int           { return len(vs) }
   126  func (vs byPkgRel) Less(i, j int) bool { return vs[i].pkgRel < vs[j].pkgRel }
   127  func (vs byPkgRel) Swap(i, j int)      { vs[i], vs[j] = vs[j], vs[i] }
   128  
   129  var genericLoads = []rule.LoadInfo{
   130  	{
   131  		Name:    "@bazel_gazelle//:def.bzl",
   132  		Symbols: []string{"gazelle"},
   133  	},
   134  }
   135  
   136  func runFixUpdate(cmd command, args []string) error {
   137  	cexts := make([]config.Configurer, 0, len(languages)+2)
   138  	cexts = append(cexts, &config.CommonConfigurer{}, &updateConfigurer{})
   139  	kindToResolver := make(map[string]resolve.Resolver)
   140  	kinds := make(map[string]rule.KindInfo)
   141  	loads := genericLoads
   142  	for _, lang := range languages {
   143  		cexts = append(cexts, lang)
   144  		for kind, info := range lang.Kinds() {
   145  			kindToResolver[kind] = lang
   146  			kinds[kind] = info
   147  		}
   148  		loads = append(loads, lang.Loads()...)
   149  	}
   150  	ruleIndex := resolve.NewRuleIndex(kindToResolver)
   151  
   152  	c, err := newFixUpdateConfiguration(cmd, args, cexts, loads)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	if cmd == fixCmd {
   158  		// Only check the version when "fix" is run. Generated build files
   159  		// frequently work with older version of rules_go, and we don't want to
   160  		// nag too much since there's no way to disable this warning.
   161  		checkRulesGoVersion(c.RepoRoot)
   162  	}
   163  
   164  	// Visit all directories in the repository.
   165  	var visits []visitRecord
   166  	walk.Walk(c, cexts, func(dir, rel string, c *config.Config, update bool, f *rule.File, subdirs, regularFiles, genFiles []string) {
   167  		// If this file is ignored or if Gazelle was not asked to update this
   168  		// directory, just index the build file and move on.
   169  		if !update {
   170  			if f != nil {
   171  				for _, r := range f.Rules {
   172  					ruleIndex.AddRule(c, r, f)
   173  				}
   174  			}
   175  			return
   176  		}
   177  
   178  		// Fix any problems in the file.
   179  		if f != nil {
   180  			for _, l := range languages {
   181  				l.Fix(c, f)
   182  			}
   183  		}
   184  
   185  		// Generate rules.
   186  		var empty, gen []*rule.Rule
   187  		for _, l := range languages {
   188  			lempty, lgen := l.GenerateRules(c, dir, rel, f, subdirs, regularFiles, genFiles, empty, gen)
   189  			empty = append(empty, lempty...)
   190  			gen = append(gen, lgen...)
   191  		}
   192  		if f == nil && len(gen) == 0 {
   193  			return
   194  		}
   195  
   196  		// Insert or merge rules into the build file.
   197  		if f == nil {
   198  			f = rule.EmptyFile(filepath.Join(dir, c.DefaultBuildFileName()), rel)
   199  			for _, r := range gen {
   200  				r.Insert(f)
   201  			}
   202  		} else {
   203  			merger.MergeFile(f, empty, gen, merger.PreResolve, kinds)
   204  		}
   205  		visits = append(visits, visitRecord{
   206  			pkgRel: rel,
   207  			rules:  gen,
   208  			empty:  empty,
   209  			file:   f,
   210  		})
   211  
   212  		// Add library rules to the dependency resolution table.
   213  		for _, r := range f.Rules {
   214  			ruleIndex.AddRule(c, r, f)
   215  		}
   216  	})
   217  
   218  	uc := getUpdateConfig(c)
   219  
   220  	// Finish building the index for dependency resolution.
   221  	ruleIndex.Finish()
   222  
   223  	// Resolve dependencies.
   224  	rc := repos.NewRemoteCache(uc.repos)
   225  	for _, v := range visits {
   226  		for _, r := range v.rules {
   227  			from := label.New(c.RepoName, v.pkgRel, r.Name())
   228  			kindToResolver[r.Kind()].Resolve(c, ruleIndex, rc, r, from)
   229  		}
   230  		merger.MergeFile(v.file, v.empty, v.rules, merger.PostResolve, kinds)
   231  	}
   232  
   233  	// Emit merged files.
   234  	for _, v := range visits {
   235  		merger.FixLoads(v.file, loads)
   236  		content := v.file.Format()
   237  		outputPath := findOutputPath(c, v.file)
   238  		if err := uc.emit(outputPath, content); err != nil {
   239  			log.Print(err)
   240  		}
   241  	}
   242  	return nil
   243  }
   244  
   245  func newFixUpdateConfiguration(cmd command, args []string, cexts []config.Configurer, loads []rule.LoadInfo) (*config.Config, error) {
   246  	c := config.New()
   247  
   248  	fs := flag.NewFlagSet("gazelle", flag.ContinueOnError)
   249  	// Flag will call this on any parse error. Don't print usage unless
   250  	// -h or -help were passed explicitly.
   251  	fs.Usage = func() {}
   252  
   253  	var knownImports []string
   254  	fs.Var(&gzflag.MultiFlag{Values: &knownImports}, "known_import", "import path for which external resolution is skipped (can specify multiple times)")
   255  
   256  	for _, cext := range cexts {
   257  		cext.RegisterFlags(fs, cmd.String(), c)
   258  	}
   259  
   260  	if err := fs.Parse(args); err != nil {
   261  		if err == flag.ErrHelp {
   262  			fixUpdateUsage(fs)
   263  			return nil, err
   264  		}
   265  		// flag already prints the error; don't print it again.
   266  		log.Fatal("Try -help for more information.")
   267  	}
   268  
   269  	for _, cext := range cexts {
   270  		if err := cext.CheckFlags(fs, c); err != nil {
   271  			return nil, err
   272  		}
   273  	}
   274  
   275  	uc := getUpdateConfig(c)
   276  	workspacePath := filepath.Join(c.RepoRoot, "WORKSPACE")
   277  	if workspace, err := rule.LoadFile(workspacePath, ""); err != nil {
   278  		if !os.IsNotExist(err) {
   279  			return nil, err
   280  		}
   281  	} else {
   282  		if err := fixWorkspace(c, workspace, loads); err != nil {
   283  			return nil, err
   284  		}
   285  		c.RepoName = findWorkspaceName(workspace)
   286  		uc.repos = repos.ListRepositories(workspace)
   287  	}
   288  	repoPrefixes := make(map[string]bool)
   289  	for _, r := range uc.repos {
   290  		repoPrefixes[r.GoPrefix] = true
   291  	}
   292  	for _, imp := range knownImports {
   293  		if repoPrefixes[imp] {
   294  			continue
   295  		}
   296  		repo := repos.Repo{
   297  			Name:     label.ImportPathToBazelRepoName(imp),
   298  			GoPrefix: imp,
   299  		}
   300  		uc.repos = append(uc.repos, repo)
   301  	}
   302  
   303  	return c, nil
   304  }
   305  
   306  func fixUpdateUsage(fs *flag.FlagSet) {
   307  	fmt.Fprint(os.Stderr, `usage: gazelle [fix|update] [flags...] [package-dirs...]
   308  
   309  The update command creates new build files and update existing BUILD files
   310  when needed.
   311  
   312  The fix command also creates and updates build files, and in addition, it may
   313  make potentially breaking updates to usage of rules. For example, it may
   314  delete obsolete rules or rename existing rules.
   315  
   316  There are several output modes which can be selected with the -mode flag. The
   317  output mode determines what Gazelle does with updated BUILD files.
   318  
   319    fix (default) - write updated BUILD files back to disk.
   320    print - print updated BUILD files to stdout.
   321    diff - diff updated BUILD files against existing files in unified format.
   322  
   323  Gazelle accepts a list of paths to Go package directories to process (defaults
   324  to the working directory if none are given). It recursively traverses
   325  subdirectories. All directories must be under the directory specified by
   326  -repo_root; if -repo_root is not given, this is the directory containing the
   327  WORKSPACE file.
   328  
   329  FLAGS:
   330  
   331  `)
   332  	fs.PrintDefaults()
   333  }
   334  
   335  func fixWorkspace(c *config.Config, workspace *rule.File, loads []rule.LoadInfo) error {
   336  	uc := getUpdateConfig(c)
   337  	if !c.ShouldFix {
   338  		return nil
   339  	}
   340  	shouldFix := false
   341  	for _, d := range c.Dirs {
   342  		if d == c.RepoRoot {
   343  			shouldFix = true
   344  		}
   345  	}
   346  	if !shouldFix {
   347  		return nil
   348  	}
   349  
   350  	merger.FixWorkspace(workspace)
   351  	merger.FixLoads(workspace, loads)
   352  	if err := merger.CheckGazelleLoaded(workspace); err != nil {
   353  		return err
   354  	}
   355  	return uc.emit(workspace.Path, workspace.Format())
   356  }
   357  
   358  func findWorkspaceName(f *rule.File) string {
   359  	for _, r := range f.Rules {
   360  		if r.Kind() == "workspace" {
   361  			return r.Name()
   362  		}
   363  	}
   364  	return ""
   365  }
   366  
   367  func isDescendingDir(dir, root string) bool {
   368  	rel, err := filepath.Rel(root, dir)
   369  	if err != nil {
   370  		return false
   371  	}
   372  	if rel == "." {
   373  		return true
   374  	}
   375  	return !strings.HasPrefix(rel, "..")
   376  }
   377  
   378  func findOutputPath(c *config.Config, f *rule.File) string {
   379  	if c.ReadBuildFilesDir == "" && c.WriteBuildFilesDir == "" {
   380  		return f.Path
   381  	}
   382  	baseDir := c.WriteBuildFilesDir
   383  	if c.WriteBuildFilesDir == "" {
   384  		baseDir = c.RepoRoot
   385  	}
   386  	outputDir := filepath.Join(baseDir, filepath.FromSlash(f.Pkg))
   387  	defaultOutputPath := filepath.Join(outputDir, c.DefaultBuildFileName())
   388  	files, err := ioutil.ReadDir(outputDir)
   389  	if err != nil {
   390  		// Ignore error. Directory probably doesn't exist.
   391  		return defaultOutputPath
   392  	}
   393  	outputPath := rule.MatchBuildFileName(outputDir, c.ValidBuildFileNames, files)
   394  	if outputPath == "" {
   395  		return defaultOutputPath
   396  	}
   397  	return outputPath
   398  }