// 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" "path/filepath" "reflect" "regexp" "strconv" "strings" "sync" "time" "unicode" "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) } // goInfo contains global information from the go tool. type goInfo struct { rootDirs map[string]string env goEnv } type goEnv struct { modulesOn bool } func determineEnv(cfg *Config) goEnv { buf, err := invokeGo(cfg, "env", "GOMOD") if err != nil { return goEnv{} } gomod := bytes.TrimSpace(buf.Bytes()) env := goEnv{} env.modulesOn = len(gomod) > 0 return env } // 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() }() } defer sizeswg.Wait() // start fetching rootDirs var info goInfo var rootDirsReady, envReady = make(chan struct{}), make(chan struct{}) go func() { info.rootDirs = determineRootDirs(cfg) close(rootDirsReady) }() go func() { info.env = determineEnv(cfg) close(envReady) }() getGoInfo := func() *goInfo { <-rootDirsReady <-envReady return &info } // Ensure that we don't leak goroutines: Load is synchronous, so callers will // not expect it to access the fields of cfg after the call returns. defer getGoInfo() // always pass getGoInfo to golistDriver golistDriver := func(cfg *Config, patterns ...string) (*driverResponse, error) { return golistDriver(cfg, getGoInfo, patterns...) } // 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, getGoInfo); 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, getGoInfo) 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, getGoInfo); err != nil { return nil, err } // Check candidate packages for containFiles. if len(containFiles) > 0 { for _, id := range containsCandidates { pkg, ok := response.seenPackages[id] if !ok { response.addPackage(&Package{ ID: id, Errors: []Error{ { Kind: ListError, Msg: fmt.Sprintf("package %s expected but not seen", id), }, }, }) continue } 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, getGoInfo func() *goInfo) 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, getGoInfo) if err != nil { return err } return addNeededOverlayPackages(cfg, driver, response, needPkgs, getGoInfo) } func runContainsQueries(cfg *Config, driver driver, response *responseDeduper, queries []string, goInfo func() *goInfo) 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 || (len(dirResponse.Packages) == 1 && len(dirResponse.Packages[0].Errors) == 1) { // There was an error loading the package. Try to load the file as an ad-hoc package. // Usually the error will appear in a returned package, but may not if we're in modules mode // and the ad-hoc is located outside a module. var queryErr error dirResponse, queryErr = driver(cfg, query) if queryErr != nil { // Return the original error if the attempt to fall back failed. return err } // If we get nothing back from `go list`, try to make this file into its own ad-hoc package. if len(dirResponse.Packages) == 0 && queryErr == nil { dirResponse.Packages = append(dirResponse.Packages, &Package{ ID: "command-line-arguments", PkgPath: query, GoFiles: []string{query}, CompiledGoFiles: []string{query}, Imports: make(map[string]*Package), }) dirResponse.Roots = append(dirResponse.Roots, "command-line-arguments") } // Special case to handle issue #33482: // If this is a file= query for ad-hoc packages where the file only exists on an overlay, // and exists outside of a module, add the file in for the package. if len(dirResponse.Packages) == 1 && (dirResponse.Packages[0].ID == "command-line-arguments" || filepath.ToSlash(dirResponse.Packages[0].PkgPath) == filepath.ToSlash(query)) { if len(dirResponse.Packages[0].GoFiles) == 0 { filename := filepath.Join(pattern, filepath.Base(query)) // avoid recomputing abspath // TODO(matloob): check if the file is outside of a root dir? for path := range cfg.Overlay { if path == filename { dirResponse.Packages[0].Errors = nil dirResponse.Packages[0].GoFiles = []string{path} dirResponse.Packages[0].CompiledGoFiles = []string{path} } } } } } 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}) cfg.Logf("%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) if err != nil { panic(err) // See above. } 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([]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{ Path: filepath.Join(goroot, "/src"), Type: gopathwalk.RootGOROOT, }) // If modules are enabled, scan the module dir. if modDir != "" { roots = append(roots, gopathwalk.Root{ Path: modDir, Type: 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{ Path: filepath.Join(p, "/pkg/mod"), Type: gopathwalk.RootModuleCache, }) } else { roots = append(roots, gopathwalk.Root{ Path: filepath.Join(p, "/src"), Type: 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, rootsDirs func() *goInfo, 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) } // Work around https://golang.org/issue/33157: // go list -e, when given an absolute path, will find the package contained at // that directory. But when no package exists there, it will return a fake package // with an error and the ImportPath set to the absolute path provided to go list. // Try to convert that absolute path to what its package path would be if it's // contained in a known module or GOPATH entry. This will allow the package to be // properly "reclaimed" when overlays are processed. if filepath.IsAbs(p.ImportPath) && p.Error != nil { pkgPath, ok := getPkgPath(cfg, p.ImportPath, rootsDirs) if ok { p.ImportPath = pkgPath } } 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 { msg := strings.TrimSpace(p.Error.Err) // Trim to work around golang.org/issue/32363. // Address golang.org/issue/35964 by appending import stack to error message. if msg == "import cycle not allowed" && len(p.Error.ImportStack) != 0 { msg += fmt.Sprintf(": import stack: %v", p.Error.ImportStack) } pkg.Errors = append(pkg.Errors, Error{ Pos: p.Error.Pos, Msg: msg, }) } response.Packages = append(response.Packages, pkg) } return &response, nil } // getPkgPath finds the package path of a directory if it's relative to a root directory. func getPkgPath(cfg *Config, dir string, goInfo func() *goInfo) (string, bool) { absDir, err := filepath.Abs(dir) if err != nil { cfg.Logf("error getting absolute path of %s: %v", dir, err) return "", false } for rdir, rpath := range goInfo().rootDirs { absRdir, err := filepath.Abs(rdir) if err != nil { cfg.Logf("error getting absolute path of %s: %v", rdir, err) continue } // Make sure that the directory is in the module, // to avoid creating a path relative to another module. if !strings.HasPrefix(absDir, absRdir) { cfg.Logf("%s does not have prefix %s", absDir, absRdir) continue } // TODO(matloob): This doesn't properly handle symlinks. r, err := filepath.Rel(rdir, dir) if err != nil { continue } if rpath != "" { // We choose only one root even though the directory even it can belong in multiple modules // or GOPATH entries. This is okay because we only need to work with absolute dirs when a // file is missing from disk, for instance when gopls calls go/packages in an overlay. // Once the file is saved, gopls, or the next invocation of the tool will get the correct // result straight from golist. // TODO(matloob): Implement module tiebreaking? return path.Join(rpath, filepath.ToSlash(r)), true } return filepath.ToSlash(r), true } return "", false } // 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&NeedImports != 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 defer func(start time.Time) { cfg.Logf("%s for %v, stderr: <<%s>> stdout: <<%s>>\n", time.Since(start), cmdDebugStr(cmd, args...), stderr, stdout) }(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)} } // Related to #24854 if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "unexpected directory layout") { return nil, fmt.Errorf("%s", stderr.String()) } // Is there an error running the C compiler in cgo? This will be reported in the "Error" field // and should be suppressed by go list -e. // // This condition is not perfect yet because the error message can include other error messages than runtime/cgo. isPkgPathRune := func(r rune) bool { // From https://golang.org/ref/spec#Import_declarations: // Implementation restriction: A compiler may restrict ImportPaths to non-empty strings // using only characters belonging to Unicode's L, M, N, P, and S general categories // (the Graphic characters without spaces) and may also exclude the // characters !"#$%&'()*,:;<=>?[\]^`{|} and the Unicode replacement character U+FFFD. return unicode.IsOneOf([]*unicode.RangeTable{unicode.L, unicode.M, unicode.N, unicode.P, unicode.S}, r) && !strings.ContainsRune("!\"#$%&'()*,:;<=>?[\\]^`{|}\uFFFD", r) } if len(stderr.String()) > 0 && strings.HasPrefix(stderr.String(), "# ") { if strings.HasPrefix(strings.TrimLeftFunc(stderr.String()[len("# "):], isPkgPathRune), "\n") { return stdout, nil } } // 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 } // Similar to the previous error, but currently lacks a fix in Go. if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "named files must all be in one directory") { output := fmt.Sprintf(`{"ImportPath": "command-line-arguments","Incomplete": true,"Error": {"Pos": "","Err": %q}}`, strings.Trim(stderr.String(), "\n")) return bytes.NewBufferString(output), nil } // Backwards compatibility for Go 1.11 because 1.12 and 1.13 put the directory in the ImportPath. // If the package doesn't exist, put the absolute path of the directory into the error message, // as Go 1.13 list does. const noSuchDirectory = "no such directory" if len(stderr.String()) > 0 && strings.Contains(stderr.String(), noSuchDirectory) { errstr := stderr.String() abspath := strings.TrimSpace(errstr[strings.Index(errstr, noSuchDirectory)+len(noSuchDirectory):]) output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`, abspath, 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. // Note that the error message we look for in this case is different that the one looked for above. 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 } // Workaround for #34273. go list -e with GO111MODULE=on has incorrect behavior when listing a // directory outside any module. if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "outside available modules") { output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`, // TODO(matloob): command-line-arguments isn't correct here. "command-line-arguments", strings.Trim(stderr.String(), "\n")) return bytes.NewBufferString(output), nil } // Another variation of the previous error if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "outside module root") { output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`, // TODO(matloob): command-line-arguments isn't correct here. "command-line-arguments", strings.Trim(stderr.String(), "\n")) return bytes.NewBufferString(output), nil } // Workaround for an instance of golang.org/issue/26755: go list -e will return a non-zero exit // status if there's a dependency on a package that doesn't exist. But it should return // a zero exit status and set an error on that package. if len(stderr.String()) > 0 && strings.Contains(stderr.String(), "no Go files in") { // Don't clobber stdout if `go list` actually returned something. if len(stdout.String()) > 0 { return stdout, nil } // try to extract package name from string stderrStr := stderr.String() var importPath string colon := strings.Index(stderrStr, ":") if colon > 0 && strings.HasPrefix(stderrStr, "go build ") { importPath = stderrStr[len("go build "):colon] } output := fmt.Sprintf(`{"ImportPath": %q,"Incomplete": true,"Error": {"Pos": "","Err": %q}}`, importPath, strings.Trim(stderrStr, "\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) } 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, " ")) }