github.com/bazelbuild/bazel-gazelle@v0.36.1-0.20240520142334-61b277ba6fed/language/go/utils.go (about) 1 /* Copyright 2022 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 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 // goListModules invokes "go list" in a directory containing a go.mod file. 37 var goListModules = func(dir string) ([]byte, error) { 38 return runGoCommandForOutput(dir, "list", "-mod=readonly", "-e", "-m", "-json", "all") 39 } 40 41 // goModDownload invokes "go mod download" in a directory containing a 42 // go.mod file. 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 // modulesFromList is an abstraction to preserve the output of `go list`. 50 // The output schema is documented at https://go.dev/ref/mod#go-list-m 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 // moduleFromDownload is an abstraction to preserve the output of `go mod download`. 65 // The output schema is documented at https://go.dev/ref/mod#go-mod-download 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 // extractModules lists all modules except for the main module, 76 // including implicit indirect dependencies. 77 func extractModules(data []byte) (map[string]*moduleFromList, error) { 78 // path@version can be used as a unique identifier for looking up sums 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 // fillMissingSums runs `go mod download` to get missing sums. 107 // This must be done in a temporary directory because 'go mod download' 108 // may modify go.mod and go.sum. It does not support -mod=readonly. 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 // Best-effort try to adorn specific error details from the JSON output. 127 for dec.More() { 128 var dl moduleFromDownload 129 if decodeErr := dec.Decode(&dl); decodeErr != nil { 130 // If we couldn't parse a possible error description, just return the raw error. 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 // toRepositoryRules transforms the input map into repository rules. 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 // processGoListError attempts a best-effort try to adorn specific error details from the JSON output of `go list`. 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 // If we couldn't parse a possible error description, just return the raw error. 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 // findGoTool attempts to locate the go executable. If GOROOT is set, we'll 202 // prefer the one in there; otherwise, we'll rely on PATH. If the wrapper 203 // script generated by the gazelle rule is invoked by Bazel, it will set 204 // GOROOT to the configured SDK. We don't want to rely on the host SDK in 205 // that situation. 206 func findGoTool() string { 207 path := "go" // rely on PATH by default 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 }