update-repos.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. /* Copyright 2017 The Bazel Authors. All rights reserved.
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package main
  13. import (
  14. "errors"
  15. "flag"
  16. "fmt"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "strings"
  21. "github.com/bazelbuild/bazel-gazelle/config"
  22. "github.com/bazelbuild/bazel-gazelle/language"
  23. "github.com/bazelbuild/bazel-gazelle/merger"
  24. "github.com/bazelbuild/bazel-gazelle/repo"
  25. "github.com/bazelbuild/bazel-gazelle/rule"
  26. )
  27. type updateReposConfig struct {
  28. repoFilePath string
  29. importPaths []string
  30. macroFileName string
  31. macroDefName string
  32. pruneRules bool
  33. workspace *rule.File
  34. repoFileMap map[string]*rule.File
  35. }
  36. const updateReposName = "_update-repos"
  37. func getUpdateReposConfig(c *config.Config) *updateReposConfig {
  38. return c.Exts[updateReposName].(*updateReposConfig)
  39. }
  40. type updateReposConfigurer struct{}
  41. type macroFlag struct {
  42. macroFileName *string
  43. macroDefName *string
  44. }
  45. func (f macroFlag) Set(value string) error {
  46. args := strings.Split(value, "%")
  47. if len(args) != 2 {
  48. return fmt.Errorf("Failure parsing to_macro: %s, expected format is macroFile%%defName", value)
  49. }
  50. if strings.HasPrefix(args[0], "..") {
  51. return fmt.Errorf("Failure parsing to_macro: %s, macro file path %s should not start with \"..\"", value, args[0])
  52. }
  53. *f.macroFileName = args[0]
  54. *f.macroDefName = args[1]
  55. return nil
  56. }
  57. func (f macroFlag) String() string {
  58. return ""
  59. }
  60. func (*updateReposConfigurer) RegisterFlags(fs *flag.FlagSet, cmd string, c *config.Config) {
  61. uc := &updateReposConfig{}
  62. c.Exts[updateReposName] = uc
  63. 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")
  64. 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")
  65. fs.BoolVar(&uc.pruneRules, "prune", false, "When enabled, Gazelle will remove rules that no longer have equivalent repos in the Gopkg.lock/go.mod file. Can only used with -from_file.")
  66. }
  67. func (*updateReposConfigurer) CheckFlags(fs *flag.FlagSet, c *config.Config) error {
  68. uc := getUpdateReposConfig(c)
  69. switch {
  70. case uc.repoFilePath != "":
  71. if len(fs.Args()) != 0 {
  72. return fmt.Errorf("got %d positional arguments with -from_file; wanted 0.\nTry -help for more information.", len(fs.Args()))
  73. }
  74. default:
  75. if len(fs.Args()) == 0 {
  76. return fmt.Errorf("no repositories specified\nTry -help for more information.")
  77. }
  78. if uc.pruneRules {
  79. return fmt.Errorf("the -prune option can only be used with -from_file")
  80. }
  81. uc.importPaths = fs.Args()
  82. }
  83. var err error
  84. workspacePath := filepath.Join(c.RepoRoot, "WORKSPACE")
  85. uc.workspace, err = rule.LoadWorkspaceFile(workspacePath, "")
  86. if err != nil {
  87. return fmt.Errorf("loading WORKSPACE file: %v", err)
  88. }
  89. c.Repos, uc.repoFileMap, err = repo.ListRepositories(uc.workspace)
  90. if err != nil {
  91. return fmt.Errorf("loading WORKSPACE file: %v", err)
  92. }
  93. return nil
  94. }
  95. func (*updateReposConfigurer) KnownDirectives() []string { return nil }
  96. func (*updateReposConfigurer) Configure(c *config.Config, rel string, f *rule.File) {}
  97. func updateRepos(args []string) (err error) {
  98. // Build configuration with all languages.
  99. cexts := make([]config.Configurer, 0, len(languages)+2)
  100. cexts = append(cexts, &config.CommonConfigurer{}, &updateReposConfigurer{})
  101. kinds := make(map[string]rule.KindInfo)
  102. loads := []rule.LoadInfo{}
  103. for _, lang := range languages {
  104. cexts = append(cexts, lang)
  105. loads = append(loads, lang.Loads()...)
  106. for kind, info := range lang.Kinds() {
  107. kinds[kind] = info
  108. }
  109. }
  110. c, err := newUpdateReposConfiguration(args, cexts)
  111. if err != nil {
  112. return err
  113. }
  114. uc := getUpdateReposConfig(c)
  115. // TODO(jayconrod): move Go-specific RemoteCache logic to language/go.
  116. var knownRepos []repo.Repo
  117. for _, r := range c.Repos {
  118. if r.Kind() == "go_repository" {
  119. knownRepos = append(knownRepos, repo.Repo{
  120. Name: r.Name(),
  121. GoPrefix: r.AttrString("importpath"),
  122. Remote: r.AttrString("remote"),
  123. VCS: r.AttrString("vcs"),
  124. })
  125. }
  126. }
  127. rc, cleanup := repo.NewRemoteCache(knownRepos)
  128. defer func() {
  129. if cerr := cleanup(); err == nil && cerr != nil {
  130. err = cerr
  131. }
  132. }()
  133. // Fix the workspace file with each language.
  134. for _, lang := range languages {
  135. lang.Fix(c, uc.workspace)
  136. }
  137. // Generate rules from command language arguments or by importing a file.
  138. var gen, empty []*rule.Rule
  139. if uc.repoFilePath == "" {
  140. gen, err = updateRepoImports(c, rc)
  141. } else {
  142. gen, empty, err = importRepos(c, rc)
  143. }
  144. if err != nil {
  145. return err
  146. }
  147. // Organize generated and empty rules by file. A rule should go into the file
  148. // it came from (by name). New rules should go into WORKSPACE or the file
  149. // specified with -to_macro.
  150. var newGen []*rule.Rule
  151. genForFiles := make(map[*rule.File][]*rule.Rule)
  152. emptyForFiles := make(map[*rule.File][]*rule.Rule)
  153. for _, r := range gen {
  154. f := uc.repoFileMap[r.Name()]
  155. if f != nil {
  156. genForFiles[f] = append(genForFiles[f], r)
  157. } else {
  158. newGen = append(newGen, r)
  159. }
  160. }
  161. for _, r := range empty {
  162. f := uc.repoFileMap[r.Name()]
  163. if f == nil {
  164. panic(fmt.Sprintf("empty rule %q for deletion that was not found", r.Name()))
  165. }
  166. emptyForFiles[f] = append(emptyForFiles[f], r)
  167. }
  168. var newGenFile *rule.File
  169. var macroPath string
  170. if uc.macroFileName != "" {
  171. macroPath = filepath.Join(c.RepoRoot, filepath.Clean(uc.macroFileName))
  172. }
  173. for f := range genForFiles {
  174. if macroPath == "" && filepath.Base(f.Path) == "WORKSPACE" ||
  175. macroPath != "" && f.Path == macroPath && f.DefName == uc.macroDefName {
  176. newGenFile = f
  177. break
  178. }
  179. }
  180. if newGenFile == nil {
  181. if uc.macroFileName == "" {
  182. newGenFile = uc.workspace
  183. } else {
  184. var err error
  185. newGenFile, err = rule.LoadMacroFile(macroPath, "", uc.macroDefName)
  186. if os.IsNotExist(err) {
  187. newGenFile, err = rule.EmptyMacroFile(macroPath, "", uc.macroDefName)
  188. if err != nil {
  189. return fmt.Errorf("error creating %q: %v", macroPath, err)
  190. }
  191. } else if err != nil {
  192. return fmt.Errorf("error loading %q: %v", macroPath, err)
  193. }
  194. }
  195. }
  196. genForFiles[newGenFile] = append(genForFiles[newGenFile], newGen...)
  197. // Merge rules and fix loads in each file.
  198. seenFile := make(map[*rule.File]bool)
  199. sortedFiles := make([]*rule.File, 0, len(genForFiles))
  200. for f := range genForFiles {
  201. if !seenFile[f] {
  202. seenFile[f] = true
  203. sortedFiles = append(sortedFiles, f)
  204. }
  205. }
  206. for f := range emptyForFiles {
  207. if !seenFile[f] {
  208. seenFile[f] = true
  209. sortedFiles = append(sortedFiles, f)
  210. }
  211. }
  212. sort.Slice(sortedFiles, func(i, j int) bool {
  213. if cmp := strings.Compare(sortedFiles[i].Path, sortedFiles[j].Path); cmp != 0 {
  214. return cmp < 0
  215. }
  216. return sortedFiles[i].DefName < sortedFiles[j].DefName
  217. })
  218. updatedFiles := make(map[string]*rule.File)
  219. for _, f := range sortedFiles {
  220. merger.MergeFile(f, emptyForFiles[f], genForFiles[f], merger.PreResolve, kinds)
  221. merger.FixLoads(f, loads)
  222. if f == uc.workspace {
  223. if err := merger.CheckGazelleLoaded(f); err != nil {
  224. return err
  225. }
  226. }
  227. f.Sync()
  228. if uf, ok := updatedFiles[f.Path]; ok {
  229. uf.SyncMacroFile(f)
  230. } else {
  231. updatedFiles[f.Path] = f
  232. }
  233. }
  234. for _, f := range sortedFiles {
  235. if uf := updatedFiles[f.Path]; uf != nil {
  236. if err := uf.Save(uf.Path); err != nil {
  237. return err
  238. }
  239. delete(updatedFiles, f.Path)
  240. }
  241. }
  242. return nil
  243. }
  244. func newUpdateReposConfiguration(args []string, cexts []config.Configurer) (*config.Config, error) {
  245. c := config.New()
  246. fs := flag.NewFlagSet("gazelle", flag.ContinueOnError)
  247. // Flag will call this on any parse error. Don't print usage unless
  248. // -h or -help were passed explicitly.
  249. fs.Usage = func() {}
  250. for _, cext := range cexts {
  251. cext.RegisterFlags(fs, "update-repos", c)
  252. }
  253. if err := fs.Parse(args); err != nil {
  254. if err == flag.ErrHelp {
  255. updateReposUsage(fs)
  256. return nil, err
  257. }
  258. // flag already prints the error; don't print it again.
  259. return nil, errors.New("Try -help for more information")
  260. }
  261. for _, cext := range cexts {
  262. if err := cext.CheckFlags(fs, c); err != nil {
  263. return nil, err
  264. }
  265. }
  266. return c, nil
  267. }
  268. func updateReposUsage(fs *flag.FlagSet) {
  269. fmt.Fprint(os.Stderr, `usage:
  270. # Add/update repositories by import path
  271. gazelle update-repos example.com/repo1 example.com/repo2
  272. # Import repositories from lock file
  273. gazelle update-repos -from_file=file
  274. The update-repos command updates repository rules in the WORKSPACE file.
  275. update-repos can add or update repositories explicitly by import path.
  276. update-repos can also import repository rules from a vendoring tool's lock
  277. file (currently only deps' Gopkg.lock is supported).
  278. FLAGS:
  279. `)
  280. fs.PrintDefaults()
  281. }
  282. func updateRepoImports(c *config.Config, rc *repo.RemoteCache) (gen []*rule.Rule, err error) {
  283. // TODO(jayconrod): let the user pick the language with a command line flag.
  284. // For now, only use the first language that implements the interface.
  285. uc := getUpdateReposConfig(c)
  286. var updater language.RepoUpdater
  287. for _, lang := range languages {
  288. if u, ok := lang.(language.RepoUpdater); ok {
  289. updater = u
  290. break
  291. }
  292. }
  293. if updater == nil {
  294. return nil, fmt.Errorf("no languages can update repositories")
  295. }
  296. res := updater.UpdateRepos(language.UpdateReposArgs{
  297. Config: c,
  298. Imports: uc.importPaths,
  299. Cache: rc,
  300. })
  301. return res.Gen, res.Error
  302. }
  303. func importRepos(c *config.Config, rc *repo.RemoteCache) (gen, empty []*rule.Rule, err error) {
  304. uc := getUpdateReposConfig(c)
  305. importSupported := false
  306. var importer language.RepoImporter
  307. for _, lang := range languages {
  308. if i, ok := lang.(language.RepoImporter); ok {
  309. importSupported = true
  310. if i.CanImport(uc.repoFilePath) {
  311. importer = i
  312. break
  313. }
  314. }
  315. }
  316. if importer == nil {
  317. if importSupported {
  318. return nil, nil, fmt.Errorf("unknown file format: %s", uc.repoFilePath)
  319. } else {
  320. return nil, nil, fmt.Errorf("no supported languages can import configuration files")
  321. }
  322. }
  323. res := importer.ImportRepos(language.ImportReposArgs{
  324. Config: c,
  325. Path: uc.repoFilePath,
  326. Prune: uc.pruneRules,
  327. Cache: rc,
  328. })
  329. return res.Gen, res.Empty, res.Error
  330. }