1
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
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
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
182 for _, lang := range filterLanguages(c, languages) {
183 lang.Fix(c, uc.workspace)
184 }
185
186
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
198
199
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
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
239
240
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
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
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
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
344
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
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
387
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
437
438
439
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
453
454
455
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
477
478
479
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
485 continue
486 }
487 if _, ok := kinds[kind]; ok {
488
489 continue
490 }
491 if r.Name() != "" {
492
493 continue
494 }
495 if idx := r.Index(); insertAfter < idx && idx < insertBefore {
496 insertBefore = idx
497 }
498 }
499
500 return insertBefore
501 }
502
503
504
505
506
507
508
509 func ensureMacroInWorkspace(uc *updateReposConfig, insertIndex int) (updated bool) {
510 if uc.macroFileName == "" {
511 return false
512 }
513
514
515
516
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
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
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
564 call.AddComment("# gazelle:repository_macro " + macroValue)
565
566 return true
567 }
568
View as plain text