| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package packages |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "go/types" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "reflect" |
| "regexp" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/go/internal/packagesdriver" |
| "golang.org/x/tools/internal/gopathwalk" |
| "golang.org/x/tools/internal/semver" |
| ) |
| |
| // debug controls verbose logging. |
| var debug, _ = strconv.ParseBool(os.Getenv("GOPACKAGESDEBUG")) |
| |
| // A goTooOldError reports that the go command |
| // found by exec.LookPath is too old to use the new go list behavior. |
| type goTooOldError struct { |
| error |
| } |
| |
| // responseDeduper wraps a driverResponse, deduplicating its contents. |
| type responseDeduper struct { |
| seenRoots map[string]bool |
| seenPackages map[string]*Package |
| dr *driverResponse |
| } |
| |
| // init fills in r with a driverResponse. |
| func (r *responseDeduper) init(dr *driverResponse) { |
| r.dr = dr |
| r.seenRoots = map[string]bool{} |
| r.seenPackages = map[string]*Package{} |
| for _, pkg := range dr.Packages { |
| r.seenPackages[pkg.ID] = pkg |
| } |
| for _, root := range dr.Roots { |
| r.seenRoots[root] = true |
| } |
| } |
| |
| func (r *responseDeduper) addPackage(p *Package) { |
| if r.seenPackages[p.ID] != nil { |
| return |
| } |
| r.seenPackages[p.ID] = p |
| r.dr.Packages = append(r.dr.Packages, p) |
| } |
| |
| func (r *responseDeduper) addRoot(id string) { |
| if r.seenRoots[id] { |
| return |
| } |
| r.seenRoots[id] = true |
| r.dr.Roots = append(r.dr.Roots, id) |
| } |
| |
| // goListDriver uses the go list command to interpret the patterns and produce |
| // the build system package structure. |
| // See driver for more details. |
| func goListDriver(cfg *Config, patterns ...string) (*driverResponse, error) { |
| var sizes types.Sizes |
| var sizeserr error |
| var sizeswg sync.WaitGroup |
| if cfg.Mode&NeedTypesSizes != 0 || cfg.Mode&NeedTypes != 0 { |
| sizeswg.Add(1) |
| go func() { |
| sizes, sizeserr = getSizes(cfg) |
| sizeswg.Done() |
| }() |
| } |
| |
| // Determine files requested in contains patterns |
| var containFiles []string |
| var packagesNamed []string |
| restPatterns := make([]string, 0, len(patterns)) |
| // Extract file= and other [querytype]= patterns. Report an error if querytype |
| // doesn't exist. |
| extractQueries: |
| for _, pattern := range patterns { |
| eqidx := strings.Index(pattern, "=") |
| if eqidx < 0 { |
| restPatterns = append(restPatterns, pattern) |
| } else { |
| query, value := pattern[:eqidx], pattern[eqidx+len("="):] |
| switch query { |
| case "file": |
| containFiles = append(containFiles, value) |
| case "pattern": |
| restPatterns = append(restPatterns, value) |
| case "iamashamedtousethedisabledqueryname": |
| packagesNamed = append(packagesNamed, value) |
| case "": // not a reserved query |
| restPatterns = append(restPatterns, pattern) |
| default: |
| for _, rune := range query { |
| if rune < 'a' || rune > 'z' { // not a reserved query |
| restPatterns = append(restPatterns, pattern) |
| continue extractQueries |
| } |
| } |
| // Reject all other patterns containing "=" |
| return nil, fmt.Errorf("invalid query type %q in query pattern %q", query, pattern) |
| } |
| } |
| } |
| |
| response := &responseDeduper{} |
| var err error |
| |
| // See if we have any patterns to pass through to go list. Zero initial |
| // patterns also requires a go list call, since it's the equivalent of |
| // ".". |
| if len(restPatterns) > 0 || len(patterns) == 0 { |
| dr, err := golistDriver(cfg, restPatterns...) |
| if err != nil { |
| return nil, err |
| } |
| response.init(dr) |
| } else { |
| response.init(&driverResponse{}) |
| } |
| |
| sizeswg.Wait() |
| if sizeserr != nil { |
| return nil, sizeserr |
| } |
| // types.SizesFor always returns nil or a *types.StdSizes |
| response.dr.Sizes, _ = sizes.(*types.StdSizes) |
| |
| var containsCandidates []string |
| |
| if len(containFiles) != 0 { |
| if err := runContainsQueries(cfg, golistDriver, response, containFiles); err != nil { |
| return nil, err |
| } |
| } |
| |
| if len(packagesNamed) != 0 { |
| if err := runNamedQueries(cfg, golistDriver, response, packagesNamed); err != nil { |
| return nil, err |
| } |
| } |
| |
| modifiedPkgs, needPkgs, err := processGolistOverlay(cfg, response.dr) |
| if err != nil { |
| return nil, err |
| } |
| if len(containFiles) > 0 { |
| containsCandidates = append(containsCandidates, modifiedPkgs...) |
| containsCandidates = append(containsCandidates, needPkgs...) |
| } |
| if err := addNeededOverlayPackages(cfg, golistDriver, response, needPkgs); err != nil { |
| return nil, err |
| } |
| // Check candidate packages for containFiles. |
| if len(containFiles) > 0 { |
| for _, id := range containsCandidates { |
| pkg := response.seenPackages[id] |
| for _, f := range containFiles { |
| for _, g := range pkg.GoFiles { |
| if sameFile(f, g) { |
| response.addRoot(id) |
| } |
| } |
| } |
| } |
| } |
| |
| return response.dr, nil |
| } |
| |
| func addNeededOverlayPackages(cfg *Config, driver driver, response *responseDeduper, pkgs []string) error { |
| if len(pkgs) == 0 { |
| return nil |
| } |
| dr, err := driver(cfg, pkgs...) |
| if err != nil { |
| return err |
| } |
| for _, pkg := range dr.Packages { |
| response.addPackage(pkg) |
| } |
| _, needPkgs, err := processGolistOverlay(cfg, response.dr) |
| if err != nil { |
| return err |
| } |
| addNeededOverlayPackages(cfg, driver, response, needPkgs) |
| return nil |
| } |
| |
| func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, queries []string) error { |
| for _, query := range queries { |
| // TODO(matloob): Do only one query per directory. |
| fdir := filepath.Dir(query) |
| // Pass absolute path of directory to go list so that it knows to treat it as a directory, |
| // not a package path. |
| pattern, err := filepath.Abs(fdir) |
| if err != nil { |
| return fmt.Errorf("could not determine absolute path of file= query path %q: %v", query, err) |
| } |
| dirResponse, err := driver(cfg, pattern) |
| if err != nil { |
| return err |
| } |
| isRoot := make(map[string]bool, len(dirResponse.Roots)) |
| for _, root := range dirResponse.Roots { |
| isRoot[root] = true |
| } |
| for _, pkg := range dirResponse.Packages { |
| // Add any new packages to the main set |
| // We don't bother to filter packages that will be dropped by the changes of roots, |
| // that will happen anyway during graph construction outside this function. |
| // Over-reporting packages is not a problem. |
| response.addPackage(pkg) |
| // if the package was not a root one, it cannot have the file |
| if !isRoot[pkg.ID] { |
| continue |
| } |
| for _, pkgFile := range pkg.GoFiles { |
| if filepath.Base(query) == filepath.Base(pkgFile) { |
| response.addRoot(pkg.ID) |
| break |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| // modCacheRegexp splits a path in a module cache into module, module version, and package. |
| var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) |
| |
| func runNamedQueries(cfg *Config, driver driver, response *responseDeduper, queries []string) error { |
| // calling `go env` isn't free; bail out if there's nothing to do. |
| if len(queries) == 0 { |
| return nil |
| } |
| // Determine which directories are relevant to scan. |
| roots, modRoot, err := roots(cfg) |
| if err != nil { |
| return err |
| } |
| |
| // Scan the selected directories. Simple matches, from GOPATH/GOROOT |
| // or the local module, can simply be "go list"ed. Matches from the |
| // module cache need special treatment. |
| var matchesMu sync.Mutex |
| var simpleMatches, modCacheMatches []string |
| add := func(root gopathwalk.Root, dir string) { |
| // Walk calls this concurrently; protect the result slices. |
| matchesMu.Lock() |
| defer matchesMu.Unlock() |
| |
| path := dir |
| if dir != root.Path { |
| path = dir[len(root.Path)+1:] |
| } |
| if pathMatchesQueries(path, queries) { |
| switch root.Type { |
| case gopathwalk.RootModuleCache: |
| modCacheMatches = append(modCacheMatches, path) |
| case gopathwalk.RootCurrentModule: |
| // We'd need to read go.mod to find the full |
| // import path. Relative's easier. |
| rel, err := filepath.Rel(cfg.Dir, dir) |
| if err != nil { |
| // This ought to be impossible, since |
| // we found dir in the current module. |
| panic(err) |
| } |
| simpleMatches = append(simpleMatches, "./"+rel) |
| case gopathwalk.RootGOPATH, gopathwalk.RootGOROOT: |
| simpleMatches = append(simpleMatches, path) |
| } |
| } |
| } |
| |
| startWalk := time.Now() |
| gopathwalk.Walk(roots, add, gopathwalk.Options{ModulesEnabled: modRoot != "", Debug: debug}) |
| if debug { |
| log.Printf("%v for walk", time.Since(startWalk)) |
| } |
| |
| // Weird special case: the top-level package in a module will be in |
| // whatever directory the user checked the repository out into. It's |
| // more reasonable for that to not match the package name. So, if there |
| // are any Go files in the mod root, query it just to be safe. |
| if modRoot != "" { |
| rel, err := filepath.Rel(cfg.Dir, modRoot) |
| if err != nil { |
| panic(err) // See above. |
| } |
| |
| files, err := ioutil.ReadDir(modRoot) |
| for _, f := range files { |
| if strings.HasSuffix(f.Name(), ".go") { |
| simpleMatches = append(simpleMatches, rel) |
| break |
| } |
| } |
| } |
| |
| addResponse := func(r *driverResponse) { |
| for _, pkg := range r.Packages { |
| response.addPackage(pkg) |
| for _, name := range queries { |
| if pkg.Name == name { |
| response.addRoot(pkg.ID) |
| break |
| } |
| } |
| } |
| } |
| |
| if len(simpleMatches) != 0 { |
| resp, err := driver(cfg, simpleMatches...) |
| if err != nil { |
| return err |
| } |
| addResponse(resp) |
| } |
| |
| // Module cache matches are tricky. We want to avoid downloading new |
| // versions of things, so we need to use the ones present in the cache. |
| // go list doesn't accept version specifiers, so we have to write out a |
| // temporary module, and do the list in that module. |
| if len(modCacheMatches) != 0 { |
| // Collect all the matches, deduplicating by major version |
| // and preferring the newest. |
| type modInfo struct { |
| mod string |
| major string |
| } |
| mods := make(map[modInfo]string) |
| var imports []string |
| for _, modPath := range modCacheMatches { |
| matches := modCacheRegexp.FindStringSubmatch(modPath) |
| mod, ver := filepath.ToSlash(matches[1]), matches[2] |
| importPath := filepath.ToSlash(filepath.Join(matches[1], matches[3])) |
| |
| major := semver.Major(ver) |
| if prevVer, ok := mods[modInfo{mod, major}]; !ok || semver.Compare(ver, prevVer) > 0 { |
| mods[modInfo{mod, major}] = ver |
| } |
| |
| imports = append(imports, importPath) |
| } |
| |
| // Build the temporary module. |
| var gomod bytes.Buffer |
| gomod.WriteString("module modquery\nrequire (\n") |
| for mod, version := range mods { |
| gomod.WriteString("\t" + mod.mod + " " + version + "\n") |
| } |
| gomod.WriteString(")\n") |
| |
| tmpCfg := *cfg |
| |
| // We're only trying to look at stuff in the module cache, so |
| // disable the network. This should speed things up, and has |
| // prevented errors in at least one case, #28518. |
| tmpCfg.Env = append(append([]string{"GOPROXY=off"}, cfg.Env...)) |
| |
| var err error |
| tmpCfg.Dir, err = ioutil.TempDir("", "gopackages-modquery") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(tmpCfg.Dir) |
| |
| if err := ioutil.WriteFile(filepath.Join(tmpCfg.Dir, "go.mod"), gomod.Bytes(), 0777); err != nil { |
| return fmt.Errorf("writing go.mod for module cache query: %v", err) |
| } |
| |
| // Run the query, using the import paths calculated from the matches above. |
| resp, err := driver(&tmpCfg, imports...) |
| if err != nil { |
| return fmt.Errorf("querying module cache matches: %v", err) |
| } |
| addResponse(resp) |
| } |
| |
| return nil |
| } |
| |
| func getSizes(cfg *Config) (types.Sizes, error) { |
| return packagesdriver.GetSizesGolist(cfg.Context, cfg.BuildFlags, cfg.Env, cfg.Dir, usesExportData(cfg)) |
| } |
| |
| // roots selects the appropriate paths to walk based on the passed-in configuration, |
| // particularly the environment and the presence of a go.mod in cfg.Dir's parents. |
| func roots(cfg *Config) ([]gopathwalk.Root, string, error) { |
| stdout, err := invokeGo(cfg, "env", "GOROOT", "GOPATH", "GOMOD") |
| if err != nil { |
| return nil, "", err |
| } |
| |
| fields := strings.Split(stdout.String(), "\n") |
| if len(fields) != 4 || len(fields[3]) != 0 { |
| return nil, "", fmt.Errorf("go env returned unexpected output: %q", stdout.String()) |
| } |
| goroot, gopath, gomod := fields[0], filepath.SplitList(fields[1]), fields[2] |
| var modDir string |
| if gomod != "" { |
| modDir = filepath.Dir(gomod) |
| } |
| |
| var roots []gopathwalk.Root |
| // Always add GOROOT. |
| roots = append(roots, gopathwalk.Root{filepath.Join(goroot, "/src"), gopathwalk.RootGOROOT}) |
| // If modules are enabled, scan the module dir. |
| if modDir != "" { |
| roots = append(roots, gopathwalk.Root{modDir, gopathwalk.RootCurrentModule}) |
| } |
| // Add either GOPATH/src or GOPATH/pkg/mod, depending on module mode. |
| for _, p := range gopath { |
| if modDir != "" { |
| roots = append(roots, gopathwalk.Root{filepath.Join(p, "/pkg/mod"), gopathwalk.RootModuleCache}) |
| } else { |
| roots = append(roots, gopathwalk.Root{filepath.Join(p, "/src"), gopathwalk.RootGOPATH}) |
| } |
| } |
| |
| return roots, modDir, nil |
| } |
| |
| // These functions were copied from goimports. See further documentation there. |
| |
| // pathMatchesQueries is adapted from pkgIsCandidate. |
| // TODO: is it reasonable to do Contains here, rather than an exact match on a path component? |
| func pathMatchesQueries(path string, queries []string) bool { |
| lastTwo := lastTwoComponents(path) |
| for _, query := range queries { |
| if strings.Contains(lastTwo, query) { |
| return true |
| } |
| if hasHyphenOrUpperASCII(lastTwo) && !hasHyphenOrUpperASCII(query) { |
| lastTwo = lowerASCIIAndRemoveHyphen(lastTwo) |
| if strings.Contains(lastTwo, query) { |
| return true |
| } |
| } |
| } |
| return false |
| } |
| |
| // lastTwoComponents returns at most the last two path components |
| // of v, using either / or \ as the path separator. |
| func lastTwoComponents(v string) string { |
| nslash := 0 |
| for i := len(v) - 1; i >= 0; i-- { |
| if v[i] == '/' || v[i] == '\\' { |
| nslash++ |
| if nslash == 2 { |
| return v[i:] |
| } |
| } |
| } |
| return v |
| } |
| |
| func hasHyphenOrUpperASCII(s string) bool { |
| for i := 0; i < len(s); i++ { |
| b := s[i] |
| if b == '-' || ('A' <= b && b <= 'Z') { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func lowerASCIIAndRemoveHyphen(s string) (ret string) { |
| buf := make([]byte, 0, len(s)) |
| for i := 0; i < len(s); i++ { |
| b := s[i] |
| switch { |
| case b == '-': |
| continue |
| case 'A' <= b && b <= 'Z': |
| buf = append(buf, b+('a'-'A')) |
| default: |
| buf = append(buf, b) |
| } |
| } |
| return string(buf) |
| } |
| |
| // Fields must match go list; |
| // see $GOROOT/src/cmd/go/internal/load/pkg.go. |
| type jsonPackage struct { |
| ImportPath string |
| Dir string |
| Name string |
| Export string |
| GoFiles []string |
| CompiledGoFiles []string |
| CFiles []string |
| CgoFiles []string |
| CXXFiles []string |
| MFiles []string |
| HFiles []string |
| FFiles []string |
| SFiles []string |
| SwigFiles []string |
| SwigCXXFiles []string |
| SysoFiles []string |
| Imports []string |
| ImportMap map[string]string |
| Deps []string |
| TestGoFiles []string |
| TestImports []string |
| XTestGoFiles []string |
| XTestImports []string |
| ForTest string // q in a "p [q.test]" package, else "" |
| DepOnly bool |
| |
| Error *jsonPackageError |
| } |
| |
| type jsonPackageError struct { |
| ImportStack []string |
| Pos string |
| Err string |
| } |
| |
| func otherFiles(p *jsonPackage) [][]string { |
| return [][]string{p.CFiles, p.CXXFiles, p.MFiles, p.HFiles, p.FFiles, p.SFiles, p.SwigFiles, p.SwigCXXFiles, p.SysoFiles} |
| } |
| |
| // golistDriver uses the "go list" command to expand the pattern |
| // words and return metadata for the specified packages. dir may be |
| // "" and env may be nil, as per os/exec.Command. |
| func golistDriver(cfg *Config, words ...string) (*driverResponse, error) { |
| // go list uses the following identifiers in ImportPath and Imports: |
| // |
| // "p" -- importable package or main (command) |
| // "q.test" -- q's test executable |
| // "p [q.test]" -- variant of p as built for q's test executable |
| // "q_test [q.test]" -- q's external test package |
| // |
| // The packages p that are built differently for a test q.test |
| // are q itself, plus any helpers used by the external test q_test, |
| // typically including "testing" and all its dependencies. |
| |
| // Run "go list" for complete |
| // information on the specified packages. |
| buf, err := invokeGo(cfg, golistargs(cfg, words)...) |
| if err != nil { |
| return nil, err |
| } |
| seen := make(map[string]*jsonPackage) |
| // Decode the JSON and convert it to Package form. |
| var response driverResponse |
| for dec := json.NewDecoder(buf); dec.More(); { |
| p := new(jsonPackage) |
| if err := dec.Decode(p); err != nil { |
| return nil, fmt.Errorf("JSON decoding failed: %v", err) |
| } |
| |
| if p.ImportPath == "" { |
| // The documentation for go list says that “[e]rroneous packages will have |
| // a non-empty ImportPath”. If for some reason it comes back empty, we |
| // prefer to error out rather than silently discarding data or handing |
| // back a package without any way to refer to it. |
| if p.Error != nil { |
| return nil, Error{ |
| Pos: p.Error.Pos, |
| Msg: p.Error.Err, |
| } |
| } |
| return nil, fmt.Errorf("package missing import path: %+v", p) |
| } |
| |
| if old, found := seen[p.ImportPath]; found { |
| if !reflect.DeepEqual(p, old) { |
| return nil, fmt.Errorf("internal error: go list gives conflicting information for package %v", p.ImportPath) |
| } |
| // skip the duplicate |
| continue |
| } |
| seen[p.ImportPath] = p |
| |
| pkg := &Package{ |
| Name: p.Name, |
| ID: p.ImportPath, |
| GoFiles: absJoin(p.Dir, p.GoFiles, p.CgoFiles), |
| CompiledGoFiles: absJoin(p.Dir, p.CompiledGoFiles), |
| OtherFiles: absJoin(p.Dir, otherFiles(p)...), |
| } |
| |
| // Work around https://golang.org/issue/28749: |
| // cmd/go puts assembly, C, and C++ files in CompiledGoFiles. |
| // Filter out any elements of CompiledGoFiles that are also in OtherFiles. |
| // We have to keep this workaround in place until go1.12 is a distant memory. |
| if len(pkg.OtherFiles) > 0 { |
| other := make(map[string]bool, len(pkg.OtherFiles)) |
| for _, f := range pkg.OtherFiles { |
| other[f] = true |
| } |
| |
| out := pkg.CompiledGoFiles[:0] |
| for _, f := range pkg.CompiledGoFiles { |
| if other[f] { |
| continue |
| } |
| out = append(out, f) |
| } |
| pkg.CompiledGoFiles = out |
| } |
| |
| // Extract the PkgPath from the package's ID. |
| if i := strings.IndexByte(pkg.ID, ' '); i >= 0 { |
| pkg.PkgPath = pkg.ID[:i] |
| } else { |
| pkg.PkgPath = pkg.ID |
| } |
| |
| if pkg.PkgPath == "unsafe" { |
| pkg.GoFiles = nil // ignore fake unsafe.go file |
| } |
| |
| // Assume go list emits only absolute paths for Dir. |
| if p.Dir != "" && !filepath.IsAbs(p.Dir) { |
| log.Fatalf("internal error: go list returned non-absolute Package.Dir: %s", p.Dir) |
| } |
| |
| if p.Export != "" && !filepath.IsAbs(p.Export) { |
| pkg.ExportFile = filepath.Join(p.Dir, p.Export) |
| } else { |
| pkg.ExportFile = p.Export |
| } |
| |
| // imports |
| // |
| // Imports contains the IDs of all imported packages. |
| // ImportsMap records (path, ID) only where they differ. |
| ids := make(map[string]bool) |
| for _, id := range p.Imports { |
| ids[id] = true |
| } |
| pkg.Imports = make(map[string]*Package) |
| for path, id := range p.ImportMap { |
| pkg.Imports[path] = &Package{ID: id} // non-identity import |
| delete(ids, id) |
| } |
| for id := range ids { |
| if id == "C" { |
| continue |
| } |
| |
| pkg.Imports[id] = &Package{ID: id} // identity import |
| } |
| if !p.DepOnly { |
| response.Roots = append(response.Roots, pkg.ID) |
| } |
| |
| // Work around for pre-go.1.11 versions of go list. |
| // TODO(matloob): they should be handled by the fallback. |
| // Can we delete this? |
| if len(pkg.CompiledGoFiles) == 0 { |
| pkg.CompiledGoFiles = pkg.GoFiles |
| } |
| |
| if p.Error != nil { |
| pkg.Errors = append(pkg.Errors, Error{ |
| Pos: p.Error.Pos, |
| Msg: p.Error.Err, |
| }) |
| } |
| |
| response.Packages = append(response.Packages, pkg) |
| } |
| |
| return &response, nil |
| } |
| |
| // absJoin absolutizes and flattens the lists of files. |
| func absJoin(dir string, fileses ...[]string) (res []string) { |
| for _, files := range fileses { |
| for _, file := range files { |
| if !filepath.IsAbs(file) { |
| file = filepath.Join(dir, file) |
| } |
| res = append(res, file) |
| } |
| } |
| return res |
| } |
| |
| func golistargs(cfg *Config, words []string) []string { |
| const findFlags = NeedImports | NeedTypes | NeedSyntax | NeedTypesInfo |
| fullargs := []string{ |
| "list", "-e", "-json", |
| fmt.Sprintf("-compiled=%t", cfg.Mode&(NeedCompiledGoFiles|NeedSyntax|NeedTypesInfo|NeedTypesSizes) != 0), |
| fmt.Sprintf("-test=%t", cfg.Tests), |
| fmt.Sprintf("-export=%t", usesExportData(cfg)), |
| fmt.Sprintf("-deps=%t", cfg.Mode&NeedDeps != 0), |
| // go list doesn't let you pass -test and -find together, |
| // probably because you'd just get the TestMain. |
| fmt.Sprintf("-find=%t", !cfg.Tests && cfg.Mode&findFlags == 0), |
| } |
| fullargs = append(fullargs, cfg.BuildFlags...) |
| fullargs = append(fullargs, "--") |
| fullargs = append(fullargs, words...) |
| return fullargs |
| } |
| |
| // invokeGo returns the stdout of a go command invocation. |
| func invokeGo(cfg *Config, args ...string) (*bytes.Buffer, error) { |
| stdout := new(bytes.Buffer) |
| stderr := new(bytes.Buffer) |
| cmd := exec.CommandContext(cfg.Context, "go", args...) |
| // On darwin the cwd gets resolved to the real path, which breaks anything that |
| // expects the working directory to keep the original path, including the |
| // go command when dealing with modules. |
| // The Go stdlib has a special feature where if the cwd and the PWD are the |
| // same node then it trusts the PWD, so by setting it in the env for the child |
| // process we fix up all the paths returned by the go command. |
| cmd.Env = append(append([]string{}, cfg.Env...), "PWD="+cfg.Dir) |
| cmd.Dir = cfg.Dir |
| cmd.Stdout = stdout |
| cmd.Stderr = stderr |
| if debug { |
| defer func(start time.Time) { |
| log.Printf("%s for %v, stderr: <<%s>>\n", time.Since(start), cmdDebugStr(cmd, args...), stderr) |
| }(time.Now()) |
| } |
| |
| if err := cmd.Run(); err != nil { |
| // Check for 'go' executable not being found. |
| if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { |
| return nil, fmt.Errorf("'go list' driver requires 'go', but %s", exec.ErrNotFound) |
| } |
| |
| exitErr, ok := err.(*exec.ExitError) |
| if !ok { |
| // Catastrophic error: |
| // - context cancellation |
| return nil, fmt.Errorf("couldn't exec 'go %v': %s %T", args, err, err) |
| } |
| |
| // Old go version? |
| if strings.Contains(stderr.String(), "flag provided but not defined") { |
| return nil, goTooOldError{fmt.Errorf("unsupported version of go: %s: %s", exitErr, stderr)} |
| } |
| |
| // This error only appears in stderr. See golang.org/cl/166398 for a fix in go list to show |
| // the error in the Err section of stdout in case -e option is provided. |
| // This fix is provided for backwards compatibility. |
| if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "named files must be .go files") { |
| output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`, |
| strings.Trim(stderr.String(), "\n")) |
| return bytes.NewBufferString(output), nil |
| } |
| |
| // Workaround for #29280: go list -e has incorrect behavior when an ad-hoc package doesn't exist. |
| if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "no such file or directory") { |
| output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`, |
| strings.Trim(stderr.String(), "\n")) |
| return bytes.NewBufferString(output), nil |
| } |
| |
| // Export mode entails a build. |
| // If that build fails, errors appear on stderr |
| // (despite the -e flag) and the Export field is blank. |
| // Do not fail in that case. |
| // The same is true if an ad-hoc package given to go list doesn't exist. |
| // TODO(matloob): Remove these once we can depend on go list to exit with a zero status with -e even when |
| // packages don't exist or a build fails. |
| if !usesExportData(cfg) && !containsGoFile(args) { |
| return nil, fmt.Errorf("go %v: %s: %s", args, exitErr, stderr) |
| } |
| } |
| |
| // As of writing, go list -export prints some non-fatal compilation |
| // errors to stderr, even with -e set. We would prefer that it put |
| // them in the Package.Error JSON (see https://golang.org/issue/26319). |
| // In the meantime, there's nowhere good to put them, but they can |
| // be useful for debugging. Print them if $GOPACKAGESPRINTGOLISTERRORS |
| // is set. |
| if len(stderr.Bytes()) != 0 && os.Getenv("GOPACKAGESPRINTGOLISTERRORS") != "" { |
| fmt.Fprintf(os.Stderr, "%s stderr: <<%s>>\n", cmdDebugStr(cmd, args...), stderr) |
| } |
| |
| // debugging |
| if false { |
| fmt.Fprintf(os.Stderr, "%s stdout: <<%s>>\n", cmdDebugStr(cmd, args...), stdout) |
| } |
| |
| return stdout, nil |
| } |
| |
| func containsGoFile(s []string) bool { |
| for _, f := range s { |
| if strings.HasSuffix(f, ".go") { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func cmdDebugStr(cmd *exec.Cmd, args ...string) string { |
| env := make(map[string]string) |
| for _, kv := range cmd.Env { |
| split := strings.Split(kv, "=") |
| k, v := split[0], split[1] |
| env[k] = v |
| } |
| var quotedArgs []string |
| for _, arg := range args { |
| quotedArgs = append(quotedArgs, strconv.Quote(arg)) |
| } |
| |
| return fmt.Sprintf("GOROOT=%v GOPATH=%v GO111MODULE=%v PWD=%v go %s", env["GOROOT"], env["GOPATH"], env["GO111MODULE"], env["PWD"], strings.Join(quotedArgs, " ")) |
| } |