modules.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. /* Copyright 2018 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 golang
  13. import (
  14. "bytes"
  15. "encoding/json"
  16. "go/build"
  17. "io"
  18. "io/ioutil"
  19. "log"
  20. "os"
  21. "os/exec"
  22. "path/filepath"
  23. "runtime"
  24. "sort"
  25. "strings"
  26. "github.com/bazelbuild/bazel-gazelle/label"
  27. "github.com/bazelbuild/bazel-gazelle/language"
  28. "github.com/bazelbuild/bazel-gazelle/rule"
  29. )
  30. func importReposFromModules(args language.ImportReposArgs) language.ImportReposResult {
  31. // Copy go.mod to temporary directory. We may run commands that modify it,
  32. // and we want to leave the original alone.
  33. tempDir, err := copyGoModToTemp(args.Path)
  34. if err != nil {
  35. return language.ImportReposResult{Error: err}
  36. }
  37. defer os.RemoveAll(tempDir)
  38. // List all modules except for the main module, including implicit indirect
  39. // dependencies.
  40. type module struct {
  41. Path, Version, Sum string
  42. Main bool
  43. Replace *struct {
  44. Path, Version string
  45. }
  46. }
  47. // path@version can be used as a unique identifier for looking up sums
  48. pathToModule := map[string]*module{}
  49. data, err := goListModules(tempDir)
  50. if err != nil {
  51. return language.ImportReposResult{Error: err}
  52. }
  53. dec := json.NewDecoder(bytes.NewReader(data))
  54. for dec.More() {
  55. mod := new(module)
  56. if err := dec.Decode(mod); err != nil {
  57. return language.ImportReposResult{Error: err}
  58. }
  59. if mod.Main {
  60. continue
  61. }
  62. if mod.Replace != nil {
  63. if filepath.IsAbs(mod.Replace.Path) || build.IsLocalImport(mod.Replace.Path) {
  64. log.Printf("go_repository does not support file path replacements for %s -> %s", mod.Path,
  65. mod.Replace.Path)
  66. continue
  67. }
  68. pathToModule[mod.Replace.Path+"@"+mod.Replace.Version] = mod
  69. } else {
  70. pathToModule[mod.Path+"@"+mod.Version] = mod
  71. }
  72. }
  73. // Load sums from go.sum. Ideally, they're all there.
  74. goSumPath := filepath.Join(filepath.Dir(args.Path), "go.sum")
  75. data, _ = ioutil.ReadFile(goSumPath)
  76. lines := bytes.Split(data, []byte("\n"))
  77. for _, line := range lines {
  78. line = bytes.TrimSpace(line)
  79. fields := bytes.Fields(line)
  80. if len(fields) != 3 {
  81. continue
  82. }
  83. path, version, sum := string(fields[0]), string(fields[1]), string(fields[2])
  84. if strings.HasSuffix(version, "/go.mod") {
  85. continue
  86. }
  87. if mod, ok := pathToModule[path+"@"+version]; ok {
  88. mod.Sum = sum
  89. }
  90. }
  91. // If sums are missing, run go mod download to get them.
  92. var missingSumArgs []string
  93. for pathVer, mod := range pathToModule {
  94. if mod.Sum == "" {
  95. missingSumArgs = append(missingSumArgs, pathVer)
  96. }
  97. }
  98. if len(missingSumArgs) > 0 {
  99. data, err := goModDownload(tempDir, missingSumArgs)
  100. if err != nil {
  101. return language.ImportReposResult{Error: err}
  102. }
  103. dec = json.NewDecoder(bytes.NewReader(data))
  104. for dec.More() {
  105. var dl module
  106. if err := dec.Decode(&dl); err != nil {
  107. return language.ImportReposResult{Error: err}
  108. }
  109. if mod, ok := pathToModule[dl.Path+"@"+dl.Version]; ok {
  110. mod.Sum = dl.Sum
  111. }
  112. }
  113. }
  114. // Translate to repository rules.
  115. gen := make([]*rule.Rule, 0, len(pathToModule))
  116. for pathVer, mod := range pathToModule {
  117. if mod.Sum == "" {
  118. log.Printf("could not determine sum for module %s", pathVer)
  119. continue
  120. }
  121. r := rule.NewRule("go_repository", label.ImportPathToBazelRepoName(mod.Path))
  122. r.SetAttr("importpath", mod.Path)
  123. r.SetAttr("sum", mod.Sum)
  124. if mod.Replace == nil {
  125. r.SetAttr("version", mod.Version)
  126. } else {
  127. r.SetAttr("replace", mod.Replace.Path)
  128. r.SetAttr("version", mod.Replace.Version)
  129. }
  130. gen = append(gen, r)
  131. }
  132. sort.Slice(gen, func(i, j int) bool {
  133. return gen[i].Name() < gen[j].Name()
  134. })
  135. return language.ImportReposResult{Gen: gen}
  136. }
  137. // goListModules invokes "go list" in a directory containing a go.mod file.
  138. var goListModules = func(dir string) ([]byte, error) {
  139. goTool := findGoTool()
  140. cmd := exec.Command(goTool, "list", "-m", "-json", "all")
  141. cmd.Stderr = os.Stderr
  142. cmd.Dir = dir
  143. return cmd.Output()
  144. }
  145. // goModDownload invokes "go mod download" in a directory containing a
  146. // go.mod file.
  147. var goModDownload = func(dir string, args []string) ([]byte, error) {
  148. goTool := findGoTool()
  149. cmd := exec.Command(goTool, "mod", "download", "-json")
  150. cmd.Args = append(cmd.Args, args...)
  151. cmd.Stderr = os.Stderr
  152. cmd.Dir = dir
  153. return cmd.Output()
  154. }
  155. // copyGoModToTemp copies to given go.mod file to a temporary directory.
  156. // go list tends to mutate go.mod files, but gazelle shouldn't do that.
  157. func copyGoModToTemp(filename string) (tempDir string, err error) {
  158. goModOrig, err := os.Open(filename)
  159. if err != nil {
  160. return "", err
  161. }
  162. defer goModOrig.Close()
  163. tempDir, err = ioutil.TempDir("", "gazelle-temp-gomod")
  164. if err != nil {
  165. return "", err
  166. }
  167. goModCopy, err := os.Create(filepath.Join(tempDir, "go.mod"))
  168. if err != nil {
  169. os.Remove(tempDir)
  170. return "", err
  171. }
  172. defer func() {
  173. if cerr := goModCopy.Close(); err == nil && cerr != nil {
  174. err = cerr
  175. }
  176. }()
  177. _, err = io.Copy(goModCopy, goModOrig)
  178. if err != nil {
  179. os.RemoveAll(tempDir)
  180. return "", err
  181. }
  182. return tempDir, err
  183. }
  184. // findGoTool attempts to locate the go executable. If GOROOT is set, we'll
  185. // prefer the one in there; otherwise, we'll rely on PATH. If the wrapper
  186. // script generated by the gazelle rule is invoked by Bazel, it will set
  187. // GOROOT to the configured SDK. We don't want to rely on the host SDK in
  188. // that situation.
  189. func findGoTool() string {
  190. path := "go" // rely on PATH by default
  191. if goroot, ok := os.LookupEnv("GOROOT"); ok {
  192. path = filepath.Join(goroot, "bin", "go")
  193. }
  194. if runtime.GOOS == "windows" {
  195. path += ".exe"
  196. }
  197. return path
  198. }