allpackages.go raw

   1  // Copyright 2014 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  
   5  // Package buildutil provides utilities related to the go/build
   6  // package in the standard library.
   7  //
   8  // All I/O is done via the build.Context file system interface, which must
   9  // be concurrency-safe.
  10  package buildutil // import "golang.org/x/tools/go/buildutil"
  11  
  12  import (
  13  	"go/build"
  14  	"os"
  15  	"path/filepath"
  16  	"sort"
  17  	"strings"
  18  	"sync"
  19  )
  20  
  21  // AllPackages returns the package path of each Go package in any source
  22  // directory of the specified build context (e.g. $GOROOT or an element
  23  // of $GOPATH).  Errors are ignored.  The results are sorted.
  24  // All package paths are canonical, and thus may contain "/vendor/".
  25  //
  26  // The result may include import paths for directories that contain no
  27  // *.go files, such as "archive" (in $GOROOT/src).
  28  //
  29  // All I/O is done via the build.Context file system interface,
  30  // which must be concurrency-safe.
  31  func AllPackages(ctxt *build.Context) []string {
  32  	var list []string
  33  	ForEachPackage(ctxt, func(pkg string, _ error) {
  34  		list = append(list, pkg)
  35  	})
  36  	sort.Strings(list)
  37  	return list
  38  }
  39  
  40  // ForEachPackage calls the found function with the package path of
  41  // each Go package it finds in any source directory of the specified
  42  // build context (e.g. $GOROOT or an element of $GOPATH).
  43  // All package paths are canonical, and thus may contain "/vendor/".
  44  //
  45  // If the package directory exists but could not be read, the second
  46  // argument to the found function provides the error.
  47  //
  48  // All I/O is done via the build.Context file system interface,
  49  // which must be concurrency-safe.
  50  func ForEachPackage(ctxt *build.Context, found func(importPath string, err error)) {
  51  	ch := make(chan item)
  52  
  53  	var wg sync.WaitGroup
  54  	for _, root := range ctxt.SrcDirs() {
  55  		wg.Add(1)
  56  		go func() {
  57  			allPackages(ctxt, root, ch)
  58  			wg.Done()
  59  		}()
  60  	}
  61  	go func() {
  62  		wg.Wait()
  63  		close(ch)
  64  	}()
  65  
  66  	// All calls to found occur in the caller's goroutine.
  67  	for i := range ch {
  68  		found(i.importPath, i.err)
  69  	}
  70  }
  71  
  72  type item struct {
  73  	importPath string
  74  	err        error // (optional)
  75  }
  76  
  77  // We use a process-wide counting semaphore to limit
  78  // the number of parallel calls to ReadDir.
  79  var ioLimit = make(chan bool, 20)
  80  
  81  func allPackages(ctxt *build.Context, root string, ch chan<- item) {
  82  	root = filepath.Clean(root) + string(os.PathSeparator)
  83  
  84  	var wg sync.WaitGroup
  85  
  86  	var walkDir func(dir string)
  87  	walkDir = func(dir string) {
  88  		// Avoid .foo, _foo, and testdata directory trees.
  89  		base := filepath.Base(dir)
  90  		if base == "" || base[0] == '.' || base[0] == '_' || base == "testdata" {
  91  			return
  92  		}
  93  
  94  		pkg := filepath.ToSlash(strings.TrimPrefix(dir, root))
  95  
  96  		// Prune search if we encounter any of these import paths.
  97  		switch pkg {
  98  		case "builtin":
  99  			return
 100  		}
 101  
 102  		ioLimit <- true
 103  		files, err := ReadDir(ctxt, dir)
 104  		<-ioLimit
 105  		if pkg != "" || err != nil {
 106  			ch <- item{pkg, err}
 107  		}
 108  		for _, fi := range files {
 109  			if fi.IsDir() {
 110  				wg.Add(1)
 111  				go func() {
 112  					walkDir(filepath.Join(dir, fi.Name()))
 113  					wg.Done()
 114  				}()
 115  			}
 116  		}
 117  	}
 118  
 119  	walkDir(root)
 120  	wg.Wait()
 121  }
 122  
 123  // ExpandPatterns returns the set of packages matched by patterns,
 124  // which may have the following forms:
 125  //
 126  //	golang.org/x/tools/cmd/guru     # a single package
 127  //	golang.org/x/tools/...          # all packages beneath dir
 128  //	...                             # the entire workspace.
 129  //
 130  // Order is significant: a pattern preceded by '-' removes matching
 131  // packages from the set.  For example, these patterns match all encoding
 132  // packages except encoding/xml:
 133  //
 134  //	encoding/... -encoding/xml
 135  //
 136  // A trailing slash in a pattern is ignored.  (Path components of Go
 137  // package names are separated by slash, not the platform's path separator.)
 138  func ExpandPatterns(ctxt *build.Context, patterns []string) map[string]bool {
 139  	// TODO(adonovan): support other features of 'go list':
 140  	// - "std"/"cmd"/"all" meta-packages
 141  	// - "..." not at the end of a pattern
 142  	// - relative patterns using "./" or "../" prefix
 143  
 144  	pkgs := make(map[string]bool)
 145  	doPkg := func(pkg string, neg bool) {
 146  		if neg {
 147  			delete(pkgs, pkg)
 148  		} else {
 149  			pkgs[pkg] = true
 150  		}
 151  	}
 152  
 153  	// Scan entire workspace if wildcards are present.
 154  	// TODO(adonovan): opt: scan only the necessary subtrees of the workspace.
 155  	var all []string
 156  	for _, arg := range patterns {
 157  		if strings.HasSuffix(arg, "...") {
 158  			all = AllPackages(ctxt)
 159  			break
 160  		}
 161  	}
 162  
 163  	for _, arg := range patterns {
 164  		if arg == "" {
 165  			continue
 166  		}
 167  
 168  		neg := arg[0] == '-'
 169  		if neg {
 170  			arg = arg[1:]
 171  		}
 172  
 173  		if arg == "..." {
 174  			// ... matches all packages
 175  			for _, pkg := range all {
 176  				doPkg(pkg, neg)
 177  			}
 178  		} else if dir, ok := strings.CutSuffix(arg, "/..."); ok {
 179  			// dir/... matches all packages beneath dir
 180  			for _, pkg := range all {
 181  				if strings.HasPrefix(pkg, dir) &&
 182  					(len(pkg) == len(dir) || pkg[len(dir)] == '/') {
 183  					doPkg(pkg, neg)
 184  				}
 185  			}
 186  		} else {
 187  			// single package
 188  			doPkg(strings.TrimSuffix(arg, "/"), neg)
 189  		}
 190  	}
 191  
 192  	return pkgs
 193  }
 194