1
15
16 package golang
17
18 import (
19 "bytes"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "go/build"
24 "log"
25 "os"
26 "os/exec"
27 "path/filepath"
28 "runtime"
29 "sort"
30 "strings"
31
32 "github.com/bazelbuild/bazel-gazelle/label"
33 "github.com/bazelbuild/bazel-gazelle/rule"
34 )
35
36
37 var goListModules = func(dir string) ([]byte, error) {
38 return runGoCommandForOutput(dir, "list", "-mod=readonly", "-e", "-m", "-json", "all")
39 }
40
41
42
43 var goModDownload = func(dir string, args []string) ([]byte, error) {
44 dlArgs := []string{"mod", "download", "-json"}
45 dlArgs = append(dlArgs, args...)
46 return runGoCommandForOutput(dir, dlArgs...)
47 }
48
49
50
51 type moduleFromList struct {
52 Path, Version, Sum string
53 Main bool
54 Replace *struct {
55 Path, Version string
56 }
57 Error *moduleError
58 }
59
60 type moduleError struct {
61 Err string
62 }
63
64
65
66 type moduleFromDownload struct {
67 Path, Version, Sum string
68 Main bool
69 Replace *struct {
70 Path, Version string
71 }
72 Error string
73 }
74
75
76
77 func extractModules(data []byte) (map[string]*moduleFromList, error) {
78
79 pathToModule := map[string]*moduleFromList{}
80 dec := json.NewDecoder(bytes.NewReader(data))
81 for dec.More() {
82 mod := new(moduleFromList)
83 if err := dec.Decode(mod); err != nil {
84 return nil, err
85 }
86 if mod.Error != nil {
87 return nil, fmt.Errorf("error listing %s: %s", mod.Path, mod.Error.Err)
88 }
89 if mod.Main {
90 continue
91 }
92 if mod.Replace != nil {
93 if filepath.IsAbs(mod.Replace.Path) || build.IsLocalImport(mod.Replace.Path) {
94 log.Printf("go_repository does not support file path replacements for %s -> %s", mod.Path,
95 mod.Replace.Path)
96 continue
97 }
98 pathToModule[mod.Replace.Path+"@"+mod.Replace.Version] = mod
99 } else {
100 pathToModule[mod.Path+"@"+mod.Version] = mod
101 }
102 }
103 return pathToModule, nil
104 }
105
106
107
108
109 func fillMissingSums(pathToModule map[string]*moduleFromList) (map[string]*moduleFromList, error) {
110 var missingSumArgs []string
111 for pathVer, mod := range pathToModule {
112 if mod.Sum == "" {
113 missingSumArgs = append(missingSumArgs, pathVer)
114 }
115 }
116
117 if len(missingSumArgs) > 0 {
118 tmpDir, err := os.MkdirTemp("", "")
119 if err != nil {
120 return nil, err
121 }
122 defer os.RemoveAll(tmpDir)
123 data, err := goModDownload(tmpDir, missingSumArgs)
124 dec := json.NewDecoder(bytes.NewReader(data))
125 if err != nil {
126
127 for dec.More() {
128 var dl moduleFromDownload
129 if decodeErr := dec.Decode(&dl); decodeErr != nil {
130
131 err = fmt.Errorf("%w\nError parsing module for more error information: %v", err, decodeErr)
132 break
133 }
134 if dl.Error != "" {
135 err = fmt.Errorf("%w\nError downloading %v: %v", err, dl.Path, dl.Error)
136 }
137 }
138 err = fmt.Errorf("error from go mod download: %w", err)
139
140 return nil, err
141 }
142 for dec.More() {
143 var dl moduleFromDownload
144 if err := dec.Decode(&dl); err != nil {
145 return nil, err
146 }
147 if mod, ok := pathToModule[dl.Path+"@"+dl.Version]; ok {
148 mod.Sum = dl.Sum
149 }
150 }
151 }
152
153 return pathToModule, nil
154 }
155
156
157 func toRepositoryRules(pathToModule map[string]*moduleFromList) []*rule.Rule {
158 gen := make([]*rule.Rule, 0, len(pathToModule))
159 for pathVer, mod := range pathToModule {
160 if mod.Sum == "" {
161 log.Printf("could not determine sum for module %s", pathVer)
162 continue
163 }
164 r := rule.NewRule("go_repository", label.ImportPathToBazelRepoName(mod.Path))
165 r.SetAttr("importpath", mod.Path)
166 r.SetAttr("sum", mod.Sum)
167 if mod.Replace == nil {
168 r.SetAttr("version", mod.Version)
169 } else {
170 r.SetAttr("replace", mod.Replace.Path)
171 r.SetAttr("version", mod.Replace.Version)
172 }
173 gen = append(gen, r)
174 }
175 sort.Slice(gen, func(i, j int) bool {
176 return gen[i].Name() < gen[j].Name()
177 })
178
179 return gen
180 }
181
182
183 func processGoListError(err error, data []byte) error {
184 dec := json.NewDecoder(bytes.NewReader(data))
185 for dec.More() {
186 var dl moduleFromList
187 if decodeErr := dec.Decode(&dl); decodeErr != nil {
188
189 err = fmt.Errorf("%w\nError parsing module for more error information: %v", err, decodeErr)
190 break
191 }
192 if dl.Error != nil {
193 err = fmt.Errorf("%w\nError listing %v: %v", err, dl.Path, dl.Error.Err)
194 }
195 }
196 err = fmt.Errorf("error from go list: %w", err)
197
198 return err
199 }
200
201
202
203
204
205
206 func findGoTool() string {
207 path := "go"
208 if goroot, ok := os.LookupEnv("GOROOT"); ok {
209 path = filepath.Join(goroot, "bin", "go")
210 }
211 if runtime.GOOS == "windows" {
212 path += ".exe"
213 }
214 return path
215 }
216
217 func runGoCommandForOutput(dir string, args ...string) ([]byte, error) {
218 goTool := findGoTool()
219 env := os.Environ()
220 env = append(env, "GO111MODULE=on")
221 if os.Getenv("GOCACHE") == "" && os.Getenv("HOME") == "" {
222 gocache, err := os.MkdirTemp("", "")
223 if err != nil {
224 return nil, err
225 }
226 env = append(env, "GOCACHE="+gocache)
227 defer os.RemoveAll(gocache)
228 }
229 if os.Getenv("GOPATH") == "" && os.Getenv("HOME") == "" {
230 gopath, err := os.MkdirTemp("", "")
231 if err != nil {
232 return nil, err
233 }
234 env = append(env, "GOPATH="+gopath)
235 defer os.RemoveAll(gopath)
236 }
237 cmd := exec.Command(goTool, args...)
238 stderr := &bytes.Buffer{}
239 cmd.Stderr = stderr
240 cmd.Dir = dir
241 cmd.Env = env
242 out, err := cmd.Output()
243 if err != nil {
244 var errStr string
245 var xerr *exec.ExitError
246 if errors.As(err, &xerr) {
247 errStr = strings.TrimSpace(stderr.String())
248 } else {
249 errStr = err.Error()
250 }
251 return out, fmt.Errorf("running '%s %s': %s", cmd.Path, strings.Join(cmd.Args, " "), errStr)
252 }
253 return out, nil
254 }
255
View as plain text