// Copyright 2017 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. // +build go1.9 package load import ( "fmt" "go/build" "log" "os" "path" "path/filepath" "regexp" "strings" ) // Context specifies values for operation of ImportPaths that would // otherwise come from cmd/go/internal/cfg package. // // This is a construct added for gotool purposes and doesn't have // an equivalent upstream in cmd/go. type Context struct { // BuildContext is the build context to use. BuildContext build.Context // GOROOTsrc is the location of the src directory in GOROOT. // At this time, it's used only in MatchPackages to skip // GOOROOT/src entry from BuildContext.SrcDirs output. GOROOTsrc string } // allPackages returns all the packages that can be found // under the $GOPATH directories and $GOROOT matching pattern. // The pattern is either "all" (all packages), "std" (standard packages), // "cmd" (standard commands), or a path including "...". func (c *Context) allPackages(pattern string) []string { pkgs := c.MatchPackages(pattern) if len(pkgs) == 0 { fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) } return pkgs } // allPackagesInFS is like allPackages but is passed a pattern // beginning ./ or ../, meaning it should scan the tree rooted // at the given directory. There are ... in the pattern too. func (c *Context) allPackagesInFS(pattern string) []string { pkgs := c.MatchPackagesInFS(pattern) if len(pkgs) == 0 { fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) } return pkgs } // MatchPackages returns a list of package paths matching pattern // (see go help packages for pattern syntax). func (c *Context) MatchPackages(pattern string) []string { match := func(string) bool { return true } treeCanMatch := func(string) bool { return true } if !IsMetaPackage(pattern) { match = matchPattern(pattern) treeCanMatch = treeCanMatchPattern(pattern) } have := map[string]bool{ "builtin": true, // ignore pseudo-package that exists only for documentation } if !c.BuildContext.CgoEnabled { have["runtime/cgo"] = true // ignore during walk } var pkgs []string for _, src := range c.BuildContext.SrcDirs() { if (pattern == "std" || pattern == "cmd") && src != c.GOROOTsrc { continue } src = filepath.Clean(src) + string(filepath.Separator) root := src if pattern == "cmd" { root += "cmd" + string(filepath.Separator) } filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { if err != nil || path == src { return nil } want := true // Avoid .foo, _foo, and testdata directory trees. _, elem := filepath.Split(path) if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" { want = false } name := filepath.ToSlash(path[len(src):]) if pattern == "std" && (!isStandardImportPath(name) || name == "cmd") { // The name "std" is only the standard library. // If the name is cmd, it's the root of the command tree. want = false } if !treeCanMatch(name) { want = false } if !fi.IsDir() { if fi.Mode()&os.ModeSymlink != 0 && want { if target, err := os.Stat(path); err == nil && target.IsDir() { fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", path) } } return nil } if !want { return filepath.SkipDir } if have[name] { return nil } have[name] = true if !match(name) { return nil } pkg, err := c.BuildContext.ImportDir(path, 0) if err != nil { if _, noGo := err.(*build.NoGoError); noGo { return nil } } // If we are expanding "cmd", skip main // packages under cmd/vendor. At least as of // March, 2017, there is one there for the // vendored pprof tool. if pattern == "cmd" && strings.HasPrefix(pkg.ImportPath, "cmd/vendor") && pkg.Name == "main" { return nil } pkgs = append(pkgs, name) return nil }) } return pkgs } // MatchPackagesInFS returns a list of package paths matching pattern, // which must begin with ./ or ../ // (see go help packages for pattern syntax). func (c *Context) MatchPackagesInFS(pattern string) []string { // Find directory to begin the scan. // Could be smarter but this one optimization // is enough for now, since ... is usually at the // end of a path. i := strings.Index(pattern, "...") dir, _ := path.Split(pattern[:i]) // pattern begins with ./ or ../. // path.Clean will discard the ./ but not the ../. // We need to preserve the ./ for pattern matching // and in the returned import paths. prefix := "" if strings.HasPrefix(pattern, "./") { prefix = "./" } match := matchPattern(pattern) var pkgs []string filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if err != nil || !fi.IsDir() { return nil } if path == dir { // filepath.Walk starts at dir and recurses. For the recursive case, // the path is the result of filepath.Join, which calls filepath.Clean. // The initial case is not Cleaned, though, so we do this explicitly. // // This converts a path like "./io/" to "io". Without this step, running // "cd $GOROOT/src; go list ./io/..." would incorrectly skip the io // package, because prepending the prefix "./" to the unclean path would // result in "././io", and match("././io") returns false. path = filepath.Clean(path) } // Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..". _, elem := filepath.Split(path) dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." if dot || strings.HasPrefix(elem, "_") || elem == "testdata" { return filepath.SkipDir } name := prefix + filepath.ToSlash(path) if !match(name) { return nil } // We keep the directory if we can import it, or if we can't import it // due to invalid Go source files. This means that directories containing // parse errors will be built (and fail) instead of being silently skipped // as not matching the pattern. Go 1.5 and earlier skipped, but that // behavior means people miss serious mistakes. // See golang.org/issue/11407. if p, err := c.BuildContext.ImportDir(path, 0); err != nil && (p == nil || len(p.InvalidGoFiles) == 0) { if _, noGo := err.(*build.NoGoError); !noGo { log.Print(err) } return nil } pkgs = append(pkgs, name) return nil }) return pkgs } // treeCanMatchPattern(pattern)(name) reports whether // name or children of name can possibly match pattern. // Pattern is the same limited glob accepted by matchPattern. func treeCanMatchPattern(pattern string) func(name string) bool { wildCard := false if i := strings.Index(pattern, "..."); i >= 0 { wildCard = true pattern = pattern[:i] } return func(name string) bool { return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || wildCard && strings.HasPrefix(name, pattern) } } // matchPattern(pattern)(name) reports whether // name matches pattern. Pattern is a limited glob // pattern in which '...' means 'any string' and there // is no other special syntax. // Unfortunately, there are two special cases. Quoting "go help packages": // // First, /... at the end of the pattern can match an empty string, // so that net/... matches both net and packages in its subdirectories, like net/http. // Second, any slash-separted pattern element containing a wildcard never // participates in a match of the "vendor" element in the path of a vendored // package, so that ./... does not match packages in subdirectories of // ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. // Note, however, that a directory named vendor that itself contains code // is not a vendored package: cmd/vendor would be a command named vendor, // and the pattern cmd/... matches it. func matchPattern(pattern string) func(name string) bool { // Convert pattern to regular expression. // The strategy for the trailing /... is to nest it in an explicit ? expression. // The strategy for the vendor exclusion is to change the unmatchable // vendor strings to a disallowed code point (vendorChar) and to use // "(anything but that codepoint)*" as the implementation of the ... wildcard. // This is a bit complicated but the obvious alternative, // namely a hand-written search like in most shell glob matchers, // is too easy to make accidentally exponential. // Using package regexp guarantees linear-time matching. const vendorChar = "\x00" if strings.Contains(pattern, vendorChar) { return func(name string) bool { return false } } re := regexp.QuoteMeta(pattern) re = replaceVendor(re, vendorChar) switch { case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` case re == vendorChar+`/\.\.\.`: re = `(/vendor|/` + vendorChar + `/\.\.\.)` case strings.HasSuffix(re, `/\.\.\.`): re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` } re = strings.Replace(re, `\.\.\.`, `[^`+vendorChar+`]*`, -1) reg := regexp.MustCompile(`^` + re + `$`) return func(name string) bool { if strings.Contains(name, vendorChar) { return false } return reg.MatchString(replaceVendor(name, vendorChar)) } } // replaceVendor returns the result of replacing // non-trailing vendor path elements in x with repl. func replaceVendor(x, repl string) string { if !strings.Contains(x, "vendor") { return x } elem := strings.Split(x, "/") for i := 0; i < len(elem)-1; i++ { if elem[i] == "vendor" { elem[i] = repl } } return strings.Join(elem, "/") } // ImportPaths returns the import paths to use for the given command line. func (c *Context) ImportPaths(args []string) []string { args = c.ImportPathsNoDotExpansion(args) var out []string for _, a := range args { if strings.Contains(a, "...") { if build.IsLocalImport(a) { out = append(out, c.allPackagesInFS(a)...) } else { out = append(out, c.allPackages(a)...) } continue } out = append(out, a) } return out } // ImportPathsNoDotExpansion returns the import paths to use for the given // command line, but it does no ... expansion. func (c *Context) ImportPathsNoDotExpansion(args []string) []string { if len(args) == 0 { return []string{"."} } var out []string for _, a := range args { // Arguments are supposed to be import paths, but // as a courtesy to Windows developers, rewrite \ to / // in command-line arguments. Handles .\... and so on. if filepath.Separator == '\\' { a = strings.Replace(a, `\`, `/`, -1) } // Put argument in canonical form, but preserve leading ./. if strings.HasPrefix(a, "./") { a = "./" + path.Clean(a) if a == "./." { a = "." } } else { a = path.Clean(a) } if IsMetaPackage(a) { out = append(out, c.allPackages(a)...) continue } out = append(out, a) } return out } // IsMetaPackage checks if name is a reserved package name that expands to multiple packages. func IsMetaPackage(name string) bool { return name == "std" || name == "cmd" || name == "all" }