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