github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/cmd/gazelle/update-repos.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  	"bytes"
    20  	"errors"
    21  	"flag"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"strings"
    27  
    28  	"github.com/bazelbuild/bazel-gazelle/config"
    29  	"github.com/bazelbuild/bazel-gazelle/internal/wspace"
    30  	"github.com/bazelbuild/bazel-gazelle/label"
    31  	"github.com/bazelbuild/bazel-gazelle/language"
    32  	"github.com/bazelbuild/bazel-gazelle/merger"
    33  	"github.com/bazelbuild/bazel-gazelle/repo"
    34  	"github.com/bazelbuild/bazel-gazelle/rule"
    35  )
    36  
    37  type updateReposConfig struct {
    38  	repoFilePath  string
    39  	importPaths   []string
    40  	macroFileName string
    41  	macroDefName  string
    42  	pruneRules    bool
    43  	workspace     *rule.File
    44  	repoFileMap   map[string]*rule.File
    45  }
    46  
    47  const updateReposName = "_update-repos"
    48  
    49  func getUpdateReposConfig(c *config.Config) *updateReposConfig {
    50  	return c.Exts[updateReposName].(*updateReposConfig)
    51  }
    52  
    53  type updateReposConfigurer struct{}
    54  
    55  type macroFlag struct {
    56  	macroFileName *string
    57  	macroDefName  *string
    58  }
    59  
    60  func (f macroFlag) Set(value string) error {
    61  	args := strings.Split(value, "%")
    62  	if len(args) != 2 {
    63  		return fmt.Errorf("Failure parsing to_macro: %s, expected format is macroFile%%defName", value)
    64  	}
    65  	if strings.HasPrefix(args[0], "..") {
    66  		return fmt.Errorf("Failure parsing to_macro: %s, macro file path %s should not start with \"..\"", value, args[0])
    67  	}
    68  	*f.macroFileName = args[0]
    69  	*f.macroDefName = args[1]
    70  	return nil
    71  }
    72  
    73  func (f macroFlag) String() string {
    74  	return ""
    75  }
    76  
    77  func (*updateReposConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {
    78  	uc := &updateReposConfig{}
    79  	c.Exts[updateReposName] = uc
    80  	fs.StringVar(&uc.repoFilePath, "from_file", "", "Gazelle will translate repositories listed in this file into repository rules in WORKSPACE or a .bzl macro function. Gopkg.lock and go.mod files are supported")
    81  	fs.Var(macroFlag{macroFileName: &uc.macroFileName, macroDefName: &uc.macroDefName}, "to_macro", "Tells Gazelle to write repository rules into a .bzl macro function rather than the WORKSPACE file. . The expected format is: macroFile%defName")
    82  	fs.BoolVar(&uc.pruneRules, "prune", false, "When enabled, Gazelle will remove rules that no longer have equivalent repos in the go.mod file. Can only used with -from_file.")
    83  }
    84  
    85  func (*updateReposConfigurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
    86  	uc := getUpdateReposConfig(c)
    87  	switch {
    88  	case uc.repoFilePath != "":
    89  		if len(fs.Args()) != 0 {
    90  			return fmt.Errorf("got %d positional arguments with -from_file; wanted 0.\nTry -help for more information.", len(fs.Args()))
    91  		}
    92  		if !filepath.IsAbs(uc.repoFilePath) {
    93  			uc.repoFilePath = filepath.Join(c.WorkDir, uc.repoFilePath)
    94  		}
    95  
    96  	default:
    97  		if len(fs.Args()) == 0 {
    98  			return fmt.Errorf("no repositories specified\nTry -help for more information.")
    99  		}
   100  		if uc.pruneRules {
   101  			return fmt.Errorf("the -prune option can only be used with -from_file")
   102  		}
   103  		uc.importPaths = fs.Args()
   104  	}
   105  
   106  	var err error
   107  	workspacePath := wspace.FindWORKSPACEFile(c.RepoRoot)
   108  	uc.workspace, err = rule.LoadWorkspaceFile(workspacePath, "")
   109  	if err != nil {
   110  		if c.Bzlmod {
   111  			return nil
   112  		} else {
   113  			return fmt.Errorf("loading WORKSPACE file: %v", err)
   114  		}
   115  	}
   116  	c.Repos, uc.repoFileMap, err = repo.ListRepositories(uc.workspace)
   117  	if err != nil {
   118  		return fmt.Errorf("loading WORKSPACE file: %v", err)
   119  	}
   120  
   121  	return nil
   122  }
   123  
   124  func (*updateReposConfigurer) KnownDirectives() []string { return nil }
   125  
   126  func (*updateReposConfigurer) Configure(c *config.Config, rel string, f *rule.File) {}
   127  
   128  func updateRepos(wd string, args []string) (err error) {
   129  	// Build configuration with all languages.
   130  	cexts := make([]config.Configurer, 0, len(languages)+2)
   131  	cexts = append(cexts, &config.CommonConfigurer{}, &updateReposConfigurer{})
   132  
   133  	for _, lang := range languages {
   134  		cexts = append(cexts, lang)
   135  	}
   136  
   137  	c, err := newUpdateReposConfiguration(wd, args, cexts)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	uc := getUpdateReposConfig(c)
   142  
   143  	kinds := make(map[string]rule.KindInfo)
   144  	loads := []rule.LoadInfo{}
   145  	for _, lang := range languages {
   146  		if moduleAwareLang, ok := lang.(language.ModuleAwareLanguage); ok {
   147  			loads = append(loads, moduleAwareLang.ApparentLoads(c.ModuleToApparentName)...)
   148  		} else {
   149  			loads = append(loads, lang.Loads()...)
   150  		}
   151  		for kind, info := range lang.Kinds() {
   152  			kinds[kind] = info
   153  		}
   154  	}
   155  
   156  	// TODO(jayconrod): move Go-specific RemoteCache logic to language/go.
   157  	var knownRepos []repo.Repo
   158  
   159  	reposFromDirectives := make(map[string]bool)
   160  	for _, r := range c.Repos {
   161  		if repo.IsFromDirective(r) {
   162  			reposFromDirectives[r.Name()] = true
   163  		}
   164  
   165  		if r.Kind() == "go_repository" {
   166  			knownRepos = append(knownRepos, repo.Repo{
   167  				Name:     r.Name(),
   168  				GoPrefix: r.AttrString("importpath"),
   169  				Remote:   r.AttrString("remote"),
   170  				VCS:      r.AttrString("vcs"),
   171  			})
   172  		}
   173  	}
   174  	rc, cleanup := repo.NewRemoteCache(knownRepos)
   175  	defer func() {
   176  		if cerr := cleanup(); err == nil && cerr != nil {
   177  			err = cerr
   178  		}
   179  	}()
   180  
   181  	// Fix the workspace file with each language.
   182  	for _, lang := range filterLanguages(c, languages) {
   183  		lang.Fix(c, uc.workspace)
   184  	}
   185  
   186  	// Generate rules from command language arguments or by importing a file.
   187  	var gen, empty []*rule.Rule
   188  	if uc.repoFilePath == "" {
   189  		gen, err = updateRepoImports(c, rc)
   190  	} else {
   191  		gen, empty, err = importRepos(c, rc)
   192  	}
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	// Organize generated and empty rules by file. A rule should go into the file
   198  	// it came from (by name). New rules should go into WORKSPACE or the file
   199  	// specified with -to_macro.
   200  	var newGen []*rule.Rule
   201  	genForFiles := make(map[*rule.File][]*rule.Rule)
   202  	emptyForFiles := make(map[*rule.File][]*rule.Rule)
   203  	genNames := make(map[string]*rule.Rule)
   204  	for _, r := range gen {
   205  
   206  		// Skip generation of rules that are defined as directives.
   207  		if reposFromDirectives[r.Name()] {
   208  			continue
   209  		}
   210  
   211  		if existingRule := genNames[r.Name()]; existingRule != nil {
   212  			import1 := existingRule.AttrString("importpath")
   213  			import2 := r.AttrString("importpath")
   214  			return fmt.Errorf("imports %s and %s resolve to the same repository rule name %s",
   215  				import1, import2, r.Name())
   216  		} else {
   217  			genNames[r.Name()] = r
   218  		}
   219  		f := uc.repoFileMap[r.Name()]
   220  		if f != nil {
   221  			genForFiles[f] = append(genForFiles[f], r)
   222  		} else {
   223  			newGen = append(newGen, r)
   224  		}
   225  	}
   226  	for _, r := range empty {
   227  		f := uc.repoFileMap[r.Name()]
   228  		if f == nil {
   229  			panic(fmt.Sprintf("empty rule %q for deletion that was not found", r.Name()))
   230  		}
   231  		emptyForFiles[f] = append(emptyForFiles[f], r)
   232  	}
   233  
   234  	var macroPath string
   235  	if uc.macroFileName != "" {
   236  		macroPath = filepath.Join(c.RepoRoot, filepath.Clean(uc.macroFileName))
   237  	}
   238  	// If we are in bzlmod mode, then do not update the workspace. However, if a macro file was
   239  	// specified, proceed with generating the macro file. This is useful for rule repositories that
   240  	// build with bzlmod enabled, but support clients that use legacy WORKSPACE dependency loading.
   241  	if !c.Bzlmod || macroPath != "" {
   242  		var newGenFile *rule.File
   243  		for f := range genForFiles {
   244  			if macroPath == "" && wspace.IsWORKSPACE(f.Path) ||
   245  				macroPath != "" && f.Path == macroPath && f.DefName == uc.macroDefName {
   246  				newGenFile = f
   247  				break
   248  			}
   249  		}
   250  		if newGenFile == nil {
   251  			if uc.macroFileName == "" {
   252  				newGenFile = uc.workspace
   253  			} else {
   254  				var err error
   255  				newGenFile, err = rule.LoadMacroFile(macroPath, "", uc.macroDefName)
   256  				if os.IsNotExist(err) {
   257  					newGenFile, err = rule.EmptyMacroFile(macroPath, "", uc.macroDefName)
   258  					if err != nil {
   259  						return fmt.Errorf("error creating %q: %v", macroPath, err)
   260  					}
   261  				} else if err != nil {
   262  					return fmt.Errorf("error loading %q: %v", macroPath, err)
   263  				}
   264  			}
   265  		}
   266  		genForFiles[newGenFile] = append(genForFiles[newGenFile], newGen...)
   267  	}
   268  
   269  	workspaceInsertIndex := findWorkspaceInsertIndex(uc.workspace, kinds, loads)
   270  	for _, r := range genForFiles[uc.workspace] {
   271  		r.SetPrivateAttr(merger.UnstableInsertIndexKey, workspaceInsertIndex)
   272  	}
   273  
   274  	// Merge rules and fix loads in each file.
   275  	seenFile := make(map[*rule.File]bool)
   276  	sortedFiles := make([]*rule.File, 0, len(genForFiles))
   277  	for f := range genForFiles {
   278  		if !seenFile[f] {
   279  			seenFile[f] = true
   280  			sortedFiles = append(sortedFiles, f)
   281  		}
   282  	}
   283  	for f := range emptyForFiles {
   284  		if !seenFile[f] {
   285  			seenFile[f] = true
   286  			sortedFiles = append(sortedFiles, f)
   287  		}
   288  	}
   289  	// If we are in bzlmod mode, then do not update the workspace.
   290  	if !c.Bzlmod && ensureMacroInWorkspace(uc, workspaceInsertIndex) {
   291  		if !seenFile[uc.workspace] {
   292  			seenFile[uc.workspace] = true
   293  			sortedFiles = append(sortedFiles, uc.workspace)
   294  		}
   295  	}
   296  	sort.Slice(sortedFiles, func(i, j int) bool {
   297  		if cmp := strings.Compare(sortedFiles[i].Path, sortedFiles[j].Path); cmp != 0 {
   298  			return cmp < 0
   299  		}
   300  		return sortedFiles[i].DefName < sortedFiles[j].DefName
   301  	})
   302  
   303  	updatedFiles := make(map[string]*rule.File)
   304  	for _, f := range sortedFiles {
   305  		merger.MergeFile(f, emptyForFiles[f], genForFiles[f], merger.PreResolve, kinds)
   306  		merger.FixLoads(f, loads)
   307  		if f == uc.workspace && !c.Bzlmod {
   308  			if err := merger.CheckGazelleLoaded(f); err != nil {
   309  				return err
   310  			}
   311  		}
   312  		f.Sync()
   313  		if uf, ok := updatedFiles[f.Path]; ok {
   314  			uf.SyncMacroFile(f)
   315  		} else {
   316  			updatedFiles[f.Path] = f
   317  		}
   318  	}
   319  
   320  	// Write updated files to disk.
   321  	for _, f := range sortedFiles {
   322  		if uf := updatedFiles[f.Path]; uf != nil {
   323  			if f.DefName != "" {
   324  				uf.SortMacro()
   325  			}
   326  			newContent := f.Format()
   327  			if !bytes.Equal(f.Content, newContent) {
   328  				if err := uf.Save(uf.Path); err != nil {
   329  					return err
   330  				}
   331  			}
   332  			delete(updatedFiles, f.Path)
   333  		}
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func newUpdateReposConfiguration(wd string, args []string, cexts []config.Configurer) (*config.Config, error) {
   340  	c := config.New()
   341  	c.WorkDir = wd
   342  	fs := flag.NewFlagSet("gazelle", flag.ContinueOnError)
   343  	// Flag will call this on any parse error. Don't print usage unless
   344  	// -h or -help were passed explicitly.
   345  	fs.Usage = func() {}
   346  	for _, cext := range cexts {
   347  		cext.RegisterFlags(fs, "update-repos", c)
   348  	}
   349  	if err := fs.Parse(args); err != nil {
   350  		if err == flag.ErrHelp {
   351  			updateReposUsage(fs)
   352  			return nil, err
   353  		}
   354  		// flag already prints the error; don't print it again.
   355  		return nil, errors.New("Try -help for more information")
   356  	}
   357  	for _, cext := range cexts {
   358  		if err := cext.CheckFlags(fs, c); err != nil {
   359  			return nil, err
   360  		}
   361  	}
   362  	return c, nil
   363  }
   364  
   365  func updateReposUsage(fs *flag.FlagSet) {
   366  	fmt.Fprint(os.Stderr, `usage:
   367  
   368  # Add/update repositories by import path
   369  gazelle update-repos example.com/repo1 example.com/repo2
   370  
   371  # Import repositories from lock file
   372  gazelle update-repos -from_file=file
   373  
   374  The update-repos command updates repository rules in the WORKSPACE file.
   375  update-repos can add or update repositories explicitly by import path.
   376  update-repos can also import repository rules from a vendoring tool's lock
   377  file (currently only deps' Gopkg.lock is supported).
   378  
   379  FLAGS:
   380  
   381  `)
   382  	fs.PrintDefaults()
   383  }
   384  
   385  func updateRepoImports(c *config.Config, rc *repo.RemoteCache) (gen []*rule.Rule, err error) {
   386  	// TODO(jayconrod): let the user pick the language with a command line flag.
   387  	// For now, only use the first language that implements the interface.
   388  	uc := getUpdateReposConfig(c)
   389  	var updater language.RepoUpdater
   390  	for _, lang := range filterLanguages(c, languages) {
   391  		if u, ok := lang.(language.RepoUpdater); ok {
   392  			updater = u
   393  			break
   394  		}
   395  	}
   396  	if updater == nil {
   397  		return nil, fmt.Errorf("no languages can update repositories")
   398  	}
   399  	res := updater.UpdateRepos(language.UpdateReposArgs{
   400  		Config:  c,
   401  		Imports: uc.importPaths,
   402  		Cache:   rc,
   403  	})
   404  	return res.Gen, res.Error
   405  }
   406  
   407  func importRepos(c *config.Config, rc *repo.RemoteCache) (gen, empty []*rule.Rule, err error) {
   408  	uc := getUpdateReposConfig(c)
   409  	importSupported := false
   410  	var importer language.RepoImporter
   411  	for _, lang := range filterLanguages(c, languages) {
   412  		if i, ok := lang.(language.RepoImporter); ok {
   413  			importSupported = true
   414  			if i.CanImport(uc.repoFilePath) {
   415  				importer = i
   416  				break
   417  			}
   418  		}
   419  	}
   420  	if importer == nil {
   421  		if importSupported {
   422  			return nil, nil, fmt.Errorf("unknown file format: %s", uc.repoFilePath)
   423  		} else {
   424  			return nil, nil, fmt.Errorf("no supported languages can import configuration files")
   425  		}
   426  	}
   427  	res := importer.ImportRepos(language.ImportReposArgs{
   428  		Config: c,
   429  		Path:   uc.repoFilePath,
   430  		Prune:  uc.pruneRules,
   431  		Cache:  rc,
   432  	})
   433  	return res.Gen, res.Empty, res.Error
   434  }
   435  
   436  // findWorkspaceInsertIndex reads a WORKSPACE file and finds an index within
   437  // f.File.Stmt where new direct dependencies should be inserted. In general, new
   438  // dependencies should be inserted after repository rules are loaded (described
   439  // by kinds) but before macros declaring indirect dependencies.
   440  func findWorkspaceInsertIndex(f *rule.File, kinds map[string]rule.KindInfo, loads []rule.LoadInfo) int {
   441  	loadFiles := make(map[string]struct{})
   442  	loadRepos := make(map[string]struct{})
   443  	for _, li := range loads {
   444  		name, err := label.Parse(li.Name)
   445  		if err != nil {
   446  			continue
   447  		}
   448  		loadFiles[li.Name] = struct{}{}
   449  		loadRepos[name.Repo] = struct{}{}
   450  	}
   451  
   452  	// Find the first index after load statements from files that contain
   453  	// repository rules (for example, "@bazel_gazelle//:deps.bzl") and after
   454  	// repository rules declaring those files (http_archive for bazel_gazelle).
   455  	// It doesn't matter whether the repository rules are actually loaded.
   456  	insertAfter := 0
   457  
   458  	for _, ld := range f.Loads {
   459  		if _, ok := loadFiles[ld.Name()]; !ok {
   460  			continue
   461  		}
   462  		if idx := ld.Index(); idx > insertAfter {
   463  			insertAfter = idx
   464  		}
   465  	}
   466  
   467  	for _, r := range f.Rules {
   468  		if _, ok := loadRepos[r.Name()]; !ok {
   469  			continue
   470  		}
   471  		if idx := r.Index(); idx > insertAfter {
   472  			insertAfter = idx
   473  		}
   474  	}
   475  
   476  	// There may be many direct dependencies after that index (perhaps
   477  	// 'update-repos' inserted them previously). We want to insert after those.
   478  	// So find the highest index after insertAfter before a call to something
   479  	// that doesn't look like a direct dependency.
   480  	insertBefore := len(f.File.Stmt)
   481  	for _, r := range f.Rules {
   482  		kind := r.Kind()
   483  		if kind == "local_repository" || kind == "http_archive" || kind == "git_repository" {
   484  			// Built-in or well-known repository rules.
   485  			continue
   486  		}
   487  		if _, ok := kinds[kind]; ok {
   488  			// Repository rule Gazelle might generate.
   489  			continue
   490  		}
   491  		if r.Name() != "" {
   492  			// Has a name attribute, probably still a repository rule.
   493  			continue
   494  		}
   495  		if idx := r.Index(); insertAfter < idx && idx < insertBefore {
   496  			insertBefore = idx
   497  		}
   498  	}
   499  
   500  	return insertBefore
   501  }
   502  
   503  // ensureMacroInWorkspace adds a call to the repository macro if the -to_macro
   504  // flag was used, and the macro was not called or declared with a
   505  // '# gazelle:repository_macro' directive.
   506  //
   507  // ensureMacroInWorkspace returns true if the WORKSPACE file was updated
   508  // and should be saved.
   509  func ensureMacroInWorkspace(uc *updateReposConfig, insertIndex int) (updated bool) {
   510  	if uc.macroFileName == "" {
   511  		return false
   512  	}
   513  
   514  	// Check whether the macro is already declared.
   515  	// We won't add a call if the macro is declared but not called. It might
   516  	// be called somewhere else.
   517  	macroValue := uc.macroFileName + "%" + uc.macroDefName
   518  	for _, d := range uc.workspace.Directives {
   519  		if d.Key == "repository_macro" {
   520  			if parsed, _ := repo.ParseRepositoryMacroDirective(d.Value); parsed != nil && parsed.Path == uc.macroFileName && parsed.DefName == uc.macroDefName {
   521  				return false
   522  			}
   523  		}
   524  	}
   525  
   526  	// Try to find a load and a call.
   527  	var load *rule.Load
   528  	var call *rule.Rule
   529  	var loadedDefName string
   530  	for _, l := range uc.workspace.Loads {
   531  		switch l.Name() {
   532  		case ":" + uc.macroFileName, "//:" + uc.macroFileName, "@//:" + uc.macroFileName:
   533  			load = l
   534  			pairs := l.SymbolPairs()
   535  			for _, pair := range pairs {
   536  				if pair.From == uc.macroDefName {
   537  					loadedDefName = pair.To
   538  				}
   539  			}
   540  		}
   541  	}
   542  
   543  	for _, r := range uc.workspace.Rules {
   544  		if r.Kind() == loadedDefName {
   545  			call = r
   546  		}
   547  	}
   548  
   549  	// Add the load and call if they're missing.
   550  	if call == nil {
   551  		if load == nil {
   552  			load = rule.NewLoad("//:" + uc.macroFileName)
   553  			load.Insert(uc.workspace, insertIndex)
   554  		}
   555  		if loadedDefName == "" {
   556  			load.Add(uc.macroDefName)
   557  		}
   558  
   559  		call = rule.NewRule(uc.macroDefName, "")
   560  		call.InsertAt(uc.workspace, insertIndex)
   561  	}
   562  
   563  	// Add the directive to the call.
   564  	call.AddComment("# gazelle:repository_macro " + macroValue)
   565  
   566  	return true
   567  }