aboutsummaryrefslogtreecommitdiff
path: root/go/buildutil/allpackages.go
blob: c0cb03e7bee375162af0bb94b715f6b574157344 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// Copyright 2014 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 buildutil provides utilities related to the go/build
// package in the standard library.
//
// All I/O is done via the build.Context file system interface, which must
// be concurrency-safe.
package buildutil // import "golang.org/x/tools/go/buildutil"

import (
	"go/build"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
)

// AllPackages returns the package path of each Go package in any source
// directory of the specified build context (e.g. $GOROOT or an element
// of $GOPATH).  Errors are ignored.  The results are sorted.
// All package paths are canonical, and thus may contain "/vendor/".
//
// The result may include import paths for directories that contain no
// *.go files, such as "archive" (in $GOROOT/src).
//
// All I/O is done via the build.Context file system interface,
// which must be concurrency-safe.
//
func AllPackages(ctxt *build.Context) []string {
	var list []string
	ForEachPackage(ctxt, func(pkg string, _ error) {
		list = append(list, pkg)
	})
	sort.Strings(list)
	return list
}

// ForEachPackage calls the found function with the package path of
// each Go package it finds in any source directory of the specified
// build context (e.g. $GOROOT or an element of $GOPATH).
// All package paths are canonical, and thus may contain "/vendor/".
//
// If the package directory exists but could not be read, the second
// argument to the found function provides the error.
//
// All I/O is done via the build.Context file system interface,
// which must be concurrency-safe.
//
func ForEachPackage(ctxt *build.Context, found func(importPath string, err error)) {
	ch := make(chan item)

	var wg sync.WaitGroup
	for _, root := range ctxt.SrcDirs() {
		root := root
		wg.Add(1)
		go func() {
			allPackages(ctxt, root, ch)
			wg.Done()
		}()
	}
	go func() {
		wg.Wait()
		close(ch)
	}()

	// All calls to found occur in the caller's goroutine.
	for i := range ch {
		found(i.importPath, i.err)
	}
}

type item struct {
	importPath string
	err        error // (optional)
}

// We use a process-wide counting semaphore to limit
// the number of parallel calls to ReadDir.
var ioLimit = make(chan bool, 20)

func allPackages(ctxt *build.Context, root string, ch chan<- item) {
	root = filepath.Clean(root) + string(os.PathSeparator)

	var wg sync.WaitGroup

	var walkDir func(dir string)
	walkDir = func(dir string) {
		// Avoid .foo, _foo, and testdata directory trees.
		base := filepath.Base(dir)
		if base == "" || base[0] == '.' || base[0] == '_' || base == "testdata" {
			return
		}

		pkg := filepath.ToSlash(strings.TrimPrefix(dir, root))

		// Prune search if we encounter any of these import paths.
		switch pkg {
		case "builtin":
			return
		}

		ioLimit <- true
		files, err := ReadDir(ctxt, dir)
		<-ioLimit
		if pkg != "" || err != nil {
			ch <- item{pkg, err}
		}
		for _, fi := range files {
			fi := fi
			if fi.IsDir() {
				wg.Add(1)
				go func() {
					walkDir(filepath.Join(dir, fi.Name()))
					wg.Done()
				}()
			}
		}
	}

	walkDir(root)
	wg.Wait()
}

// ExpandPatterns returns the set of packages matched by patterns,
// which may have the following forms:
//
//		golang.org/x/tools/cmd/guru     # a single package
//		golang.org/x/tools/...          # all packages beneath dir
//		...                             # the entire workspace.
//
// Order is significant: a pattern preceded by '-' removes matching
// packages from the set.  For example, these patterns match all encoding
// packages except encoding/xml:
//
// 	encoding/... -encoding/xml
//
// A trailing slash in a pattern is ignored.  (Path components of Go
// package names are separated by slash, not the platform's path separator.)
//
func ExpandPatterns(ctxt *build.Context, patterns []string) map[string]bool {
	// TODO(adonovan): support other features of 'go list':
	// - "std"/"cmd"/"all" meta-packages
	// - "..." not at the end of a pattern
	// - relative patterns using "./" or "../" prefix

	pkgs := make(map[string]bool)
	doPkg := func(pkg string, neg bool) {
		if neg {
			delete(pkgs, pkg)
		} else {
			pkgs[pkg] = true
		}
	}

	// Scan entire workspace if wildcards are present.
	// TODO(adonovan): opt: scan only the necessary subtrees of the workspace.
	var all []string
	for _, arg := range patterns {
		if strings.HasSuffix(arg, "...") {
			all = AllPackages(ctxt)
			break
		}
	}

	for _, arg := range patterns {
		if arg == "" {
			continue
		}

		neg := arg[0] == '-'
		if neg {
			arg = arg[1:]
		}

		if arg == "..." {
			// ... matches all packages
			for _, pkg := range all {
				doPkg(pkg, neg)
			}
		} else if dir := strings.TrimSuffix(arg, "/..."); dir != arg {
			// dir/... matches all packages beneath dir
			for _, pkg := range all {
				if strings.HasPrefix(pkg, dir) &&
					(len(pkg) == len(dir) || pkg[len(dir)] == '/') {
					doPkg(pkg, neg)
				}
			}
		} else {
			// single package
			doPkg(strings.TrimSuffix(arg, "/"), neg)
		}
	}

	return pkgs
}