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 }