1// Copyright 2015 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package main 6 7import ( 8 "bytes" 9 "fmt" 10 "log" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "sync" 17 18 "golang.org/x/mod/semver" 19) 20 21// A Dir describes a directory holding code by specifying 22// the expected import path and the file system directory. 23type Dir struct { 24 importPath string // import path for that dir 25 dir string // file system directory 26 inModule bool 27} 28 29// Dirs is a structure for scanning the directory tree. 30// Its Next method returns the next Go source directory it finds. 31// Although it can be used to scan the tree multiple times, it 32// only walks the tree once, caching the data it finds. 33type Dirs struct { 34 scan chan Dir // Directories generated by walk. 35 hist []Dir // History of reported Dirs. 36 offset int // Counter for Next. 37} 38 39var dirs Dirs 40 41// dirsInit starts the scanning of package directories in GOROOT and GOPATH. Any 42// extra paths passed to it are included in the channel. 43func dirsInit(extra ...Dir) { 44 if buildCtx.GOROOT == "" { 45 stdout, err := exec.Command("go", "env", "GOROOT").Output() 46 if err != nil { 47 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { 48 log.Fatalf("failed to determine GOROOT: $GOROOT is not set and 'go env GOROOT' failed:\n%s", ee.Stderr) 49 } 50 log.Fatalf("failed to determine GOROOT: $GOROOT is not set and could not run 'go env GOROOT':\n\t%s", err) 51 } 52 buildCtx.GOROOT = string(bytes.TrimSpace(stdout)) 53 } 54 55 dirs.hist = make([]Dir, 0, 1000) 56 dirs.hist = append(dirs.hist, extra...) 57 dirs.scan = make(chan Dir) 58 go dirs.walk(codeRoots()) 59} 60 61// goCmd returns the "go" command path corresponding to buildCtx.GOROOT. 62func goCmd() string { 63 if buildCtx.GOROOT == "" { 64 return "go" 65 } 66 return filepath.Join(buildCtx.GOROOT, "bin", "go") 67} 68 69// Reset puts the scan back at the beginning. 70func (d *Dirs) Reset() { 71 d.offset = 0 72} 73 74// Next returns the next directory in the scan. The boolean 75// is false when the scan is done. 76func (d *Dirs) Next() (Dir, bool) { 77 if d.offset < len(d.hist) { 78 dir := d.hist[d.offset] 79 d.offset++ 80 return dir, true 81 } 82 dir, ok := <-d.scan 83 if !ok { 84 return Dir{}, false 85 } 86 d.hist = append(d.hist, dir) 87 d.offset++ 88 return dir, ok 89} 90 91// walk walks the trees in GOROOT and GOPATH. 92func (d *Dirs) walk(roots []Dir) { 93 for _, root := range roots { 94 d.bfsWalkRoot(root) 95 } 96 close(d.scan) 97} 98 99// bfsWalkRoot walks a single directory hierarchy in breadth-first lexical order. 100// Each Go source directory it finds is delivered on d.scan. 101func (d *Dirs) bfsWalkRoot(root Dir) { 102 root.dir = filepath.Clean(root.dir) // because filepath.Join will do it anyway 103 104 // this is the queue of directories to examine in this pass. 105 this := []string{} 106 // next is the queue of directories to examine in the next pass. 107 next := []string{root.dir} 108 109 for len(next) > 0 { 110 this, next = next, this[0:0] 111 for _, dir := range this { 112 fd, err := os.Open(dir) 113 if err != nil { 114 log.Print(err) 115 continue 116 } 117 entries, err := fd.Readdir(0) 118 fd.Close() 119 if err != nil { 120 log.Print(err) 121 continue 122 } 123 hasGoFiles := false 124 for _, entry := range entries { 125 name := entry.Name() 126 // For plain files, remember if this directory contains any .go 127 // source files, but ignore them otherwise. 128 if !entry.IsDir() { 129 if !hasGoFiles && strings.HasSuffix(name, ".go") { 130 hasGoFiles = true 131 } 132 continue 133 } 134 // Entry is a directory. 135 136 // The go tool ignores directories starting with ., _, or named "testdata". 137 if name[0] == '.' || name[0] == '_' || name == "testdata" { 138 continue 139 } 140 // When in a module, ignore vendor directories and stop at module boundaries. 141 if root.inModule { 142 if name == "vendor" { 143 continue 144 } 145 if fi, err := os.Stat(filepath.Join(dir, name, "go.mod")); err == nil && !fi.IsDir() { 146 continue 147 } 148 } 149 // Remember this (fully qualified) directory for the next pass. 150 next = append(next, filepath.Join(dir, name)) 151 } 152 if hasGoFiles { 153 // It's a candidate. 154 importPath := root.importPath 155 if len(dir) > len(root.dir) { 156 if importPath != "" { 157 importPath += "/" 158 } 159 importPath += filepath.ToSlash(dir[len(root.dir)+1:]) 160 } 161 d.scan <- Dir{importPath, dir, root.inModule} 162 } 163 } 164 165 } 166} 167 168var testGOPATH = false // force GOPATH use for testing 169 170// codeRoots returns the code roots to search for packages. 171// In GOPATH mode this is GOROOT/src and GOPATH/src, with empty import paths. 172// In module mode, this is each module root, with an import path set to its module path. 173func codeRoots() []Dir { 174 codeRootsCache.once.Do(func() { 175 codeRootsCache.roots = findCodeRoots() 176 }) 177 return codeRootsCache.roots 178} 179 180var codeRootsCache struct { 181 once sync.Once 182 roots []Dir 183} 184 185var usingModules bool 186 187func findCodeRoots() []Dir { 188 var list []Dir 189 if !testGOPATH { 190 // Check for use of modules by 'go env GOMOD', 191 // which reports a go.mod file path if modules are enabled. 192 stdout, _ := exec.Command(goCmd(), "env", "GOMOD").Output() 193 gomod := string(bytes.TrimSpace(stdout)) 194 195 usingModules = len(gomod) > 0 196 if usingModules && buildCtx.GOROOT != "" { 197 list = append(list, 198 Dir{dir: filepath.Join(buildCtx.GOROOT, "src"), inModule: true}, 199 Dir{importPath: "cmd", dir: filepath.Join(buildCtx.GOROOT, "src", "cmd"), inModule: true}) 200 } 201 202 if gomod == os.DevNull { 203 // Modules are enabled, but the working directory is outside any module. 204 // We can still access std, cmd, and packages specified as source files 205 // on the command line, but there are no module roots. 206 // Avoid 'go list -m all' below, since it will not work. 207 return list 208 } 209 } 210 211 if !usingModules { 212 if buildCtx.GOROOT != "" { 213 list = append(list, Dir{dir: filepath.Join(buildCtx.GOROOT, "src")}) 214 } 215 for _, root := range splitGopath() { 216 list = append(list, Dir{dir: filepath.Join(root, "src")}) 217 } 218 return list 219 } 220 221 // Find module root directories from go list. 222 // Eventually we want golang.org/x/tools/go/packages 223 // to handle the entire file system search and become go/packages, 224 // but for now enumerating the module roots lets us fit modules 225 // into the current code with as few changes as possible. 226 mainMod, vendorEnabled, err := vendorEnabled() 227 if err != nil { 228 return list 229 } 230 if vendorEnabled { 231 // Add the vendor directory to the search path ahead of "std". 232 // That way, if the main module *is* "std", we will identify the path 233 // without the "vendor/" prefix before the one with that prefix. 234 list = append([]Dir{{dir: filepath.Join(mainMod.Dir, "vendor"), inModule: false}}, list...) 235 if mainMod.Path != "std" { 236 list = append(list, Dir{importPath: mainMod.Path, dir: mainMod.Dir, inModule: true}) 237 } 238 return list 239 } 240 241 cmd := exec.Command(goCmd(), "list", "-m", "-f={{.Path}}\t{{.Dir}}", "all") 242 cmd.Stderr = os.Stderr 243 out, _ := cmd.Output() 244 for _, line := range strings.Split(string(out), "\n") { 245 path, dir, _ := strings.Cut(line, "\t") 246 if dir != "" { 247 list = append(list, Dir{importPath: path, dir: dir, inModule: true}) 248 } 249 } 250 251 return list 252} 253 254// The functions below are derived from x/tools/internal/imports at CL 203017. 255 256type moduleJSON struct { 257 Path, Dir, GoVersion string 258} 259 260var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) 261 262// vendorEnabled indicates if vendoring is enabled. 263// Inspired by setDefaultBuildMod in modload/init.go 264func vendorEnabled() (*moduleJSON, bool, error) { 265 mainMod, go114, err := getMainModuleAnd114() 266 if err != nil { 267 return nil, false, err 268 } 269 270 stdout, _ := exec.Command(goCmd(), "env", "GOFLAGS").Output() 271 goflags := string(bytes.TrimSpace(stdout)) 272 matches := modFlagRegexp.FindStringSubmatch(goflags) 273 var modFlag string 274 if len(matches) != 0 { 275 modFlag = matches[1] 276 } 277 if modFlag != "" { 278 // Don't override an explicit '-mod=' argument. 279 return mainMod, modFlag == "vendor", nil 280 } 281 if mainMod == nil || !go114 { 282 return mainMod, false, nil 283 } 284 // Check 1.14's automatic vendor mode. 285 if fi, err := os.Stat(filepath.Join(mainMod.Dir, "vendor")); err == nil && fi.IsDir() { 286 if mainMod.GoVersion != "" && semver.Compare("v"+mainMod.GoVersion, "v1.14") >= 0 { 287 // The Go version is at least 1.14, and a vendor directory exists. 288 // Set -mod=vendor by default. 289 return mainMod, true, nil 290 } 291 } 292 return mainMod, false, nil 293} 294 295// getMainModuleAnd114 gets the main module's information and whether the 296// go command in use is 1.14+. This is the information needed to figure out 297// if vendoring should be enabled. 298func getMainModuleAnd114() (*moduleJSON, bool, error) { 299 const format = `{{.Path}} 300{{.Dir}} 301{{.GoVersion}} 302{{range context.ReleaseTags}}{{if eq . "go1.14"}}{{.}}{{end}}{{end}} 303` 304 cmd := exec.Command(goCmd(), "list", "-m", "-f", format) 305 cmd.Stderr = os.Stderr 306 stdout, err := cmd.Output() 307 if err != nil { 308 return nil, false, nil 309 } 310 lines := strings.Split(string(stdout), "\n") 311 if len(lines) < 5 { 312 return nil, false, fmt.Errorf("unexpected stdout: %q", stdout) 313 } 314 mod := &moduleJSON{ 315 Path: lines[0], 316 Dir: lines[1], 317 GoVersion: lines[2], 318 } 319 return mod, lines[3] == "go1.14", nil 320} 321