1// Copyright 2018 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
5package modload
6
7import (
8	"context"
9	"errors"
10	"fmt"
11	"io/fs"
12	"os"
13	"path"
14	"path/filepath"
15	"runtime"
16	"sort"
17	"strings"
18	"sync"
19
20	"cmd/go/internal/cfg"
21	"cmd/go/internal/fsys"
22	"cmd/go/internal/gover"
23	"cmd/go/internal/imports"
24	"cmd/go/internal/modindex"
25	"cmd/go/internal/par"
26	"cmd/go/internal/search"
27	"cmd/go/internal/str"
28	"cmd/go/internal/trace"
29	"cmd/internal/pkgpattern"
30
31	"golang.org/x/mod/module"
32)
33
34type stdFilter int8
35
36const (
37	omitStd = stdFilter(iota)
38	includeStd
39)
40
41// matchPackages is like m.MatchPackages, but uses a local variable (rather than
42// a global) for tags, can include or exclude packages in the standard library,
43// and is restricted to the given list of modules.
44func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, filter stdFilter, modules []module.Version) {
45	ctx, span := trace.StartSpan(ctx, "modload.matchPackages")
46	defer span.Done()
47
48	m.Pkgs = []string{}
49
50	isMatch := func(string) bool { return true }
51	treeCanMatch := func(string) bool { return true }
52	if !m.IsMeta() {
53		isMatch = pkgpattern.MatchPattern(m.Pattern())
54		treeCanMatch = pkgpattern.TreeCanMatchPattern(m.Pattern())
55	}
56
57	var mu sync.Mutex
58	have := map[string]bool{
59		"builtin": true, // ignore pseudo-package that exists only for documentation
60	}
61	addPkg := func(p string) {
62		mu.Lock()
63		m.Pkgs = append(m.Pkgs, p)
64		mu.Unlock()
65	}
66	if !cfg.BuildContext.CgoEnabled {
67		have["runtime/cgo"] = true // ignore during walk
68	}
69
70	type pruning int8
71	const (
72		pruneVendor = pruning(1 << iota)
73		pruneGoMod
74	)
75
76	q := par.NewQueue(runtime.GOMAXPROCS(0))
77
78	walkPkgs := func(root, importPathRoot string, prune pruning) {
79		_, span := trace.StartSpan(ctx, "walkPkgs "+root)
80		defer span.Done()
81
82		// If the root itself is a symlink to a directory,
83		// we want to follow it (see https://go.dev/issue/50807).
84		// Add a trailing separator to force that to happen.
85		root = str.WithFilePathSeparator(filepath.Clean(root))
86		err := fsys.Walk(root, func(pkgDir string, fi fs.FileInfo, err error) error {
87			if err != nil {
88				m.AddError(err)
89				return nil
90			}
91
92			want := true
93			elem := ""
94
95			// Don't use GOROOT/src but do walk down into it.
96			if pkgDir == root {
97				if importPathRoot == "" {
98					return nil
99				}
100			} else {
101				// Avoid .foo, _foo, and testdata subdirectory trees.
102				_, elem = filepath.Split(pkgDir)
103				if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
104					want = false
105				}
106			}
107
108			name := path.Join(importPathRoot, filepath.ToSlash(pkgDir[len(root):]))
109			if !treeCanMatch(name) {
110				want = false
111			}
112
113			if !fi.IsDir() {
114				if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.Pattern(), "...") {
115					if target, err := fsys.Stat(pkgDir); err == nil && target.IsDir() {
116						fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", pkgDir)
117					}
118				}
119				return nil
120			}
121
122			if !want {
123				return filepath.SkipDir
124			}
125			// Stop at module boundaries.
126			if (prune&pruneGoMod != 0) && pkgDir != root {
127				if fi, err := os.Stat(filepath.Join(pkgDir, "go.mod")); err == nil && !fi.IsDir() {
128					return filepath.SkipDir
129				}
130			}
131
132			if !have[name] {
133				have[name] = true
134				if isMatch(name) {
135					q.Add(func() {
136						if _, _, err := scanDir(root, pkgDir, tags); err != imports.ErrNoGo {
137							addPkg(name)
138						}
139					})
140				}
141			}
142
143			if elem == "vendor" && (prune&pruneVendor != 0) {
144				return filepath.SkipDir
145			}
146			return nil
147		})
148		if err != nil {
149			m.AddError(err)
150		}
151	}
152
153	// Wait for all in-flight operations to complete before returning.
154	defer func() {
155		<-q.Idle()
156		sort.Strings(m.Pkgs) // sort everything we added for determinism
157	}()
158
159	if filter == includeStd {
160		walkPkgs(cfg.GOROOTsrc, "", pruneGoMod)
161		if treeCanMatch("cmd") {
162			walkPkgs(filepath.Join(cfg.GOROOTsrc, "cmd"), "cmd", pruneGoMod)
163		}
164	}
165
166	if cfg.BuildMod == "vendor" {
167		for _, mod := range MainModules.Versions() {
168			if modRoot := MainModules.ModRoot(mod); modRoot != "" {
169				walkPkgs(modRoot, MainModules.PathPrefix(mod), pruneGoMod|pruneVendor)
170			}
171		}
172		if HasModRoot() {
173			walkPkgs(VendorDir(), "", pruneVendor)
174		}
175		return
176	}
177
178	for _, mod := range modules {
179		if gover.IsToolchain(mod.Path) || !treeCanMatch(mod.Path) {
180			continue
181		}
182
183		var (
184			root, modPrefix string
185			isLocal         bool
186		)
187		if MainModules.Contains(mod.Path) {
188			if MainModules.ModRoot(mod) == "" {
189				continue // If there is no main module, we can't search in it.
190			}
191			root = MainModules.ModRoot(mod)
192			modPrefix = MainModules.PathPrefix(mod)
193			isLocal = true
194		} else {
195			var err error
196			root, isLocal, err = fetch(ctx, mod)
197			if err != nil {
198				m.AddError(err)
199				continue
200			}
201			modPrefix = mod.Path
202		}
203		if mi, err := modindex.GetModule(root); err == nil {
204			walkFromIndex(mi, modPrefix, isMatch, treeCanMatch, tags, have, addPkg)
205			continue
206		} else if !errors.Is(err, modindex.ErrNotIndexed) {
207			m.AddError(err)
208		}
209
210		prune := pruneVendor
211		if isLocal {
212			prune |= pruneGoMod
213		}
214		walkPkgs(root, modPrefix, prune)
215	}
216}
217
218// walkFromIndex matches packages in a module using the module index. modroot
219// is the module's root directory on disk, index is the modindex.Module for the
220// module, and importPathRoot is the module's path prefix.
221func walkFromIndex(index *modindex.Module, importPathRoot string, isMatch, treeCanMatch func(string) bool, tags, have map[string]bool, addPkg func(string)) {
222	index.Walk(func(reldir string) {
223		// Avoid .foo, _foo, and testdata subdirectory trees.
224		p := reldir
225		for {
226			elem, rest, found := strings.Cut(p, string(filepath.Separator))
227			if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
228				return
229			}
230			if found && elem == "vendor" {
231				// Ignore this path if it contains the element "vendor" anywhere
232				// except for the last element (packages named vendor are allowed
233				// for historical reasons). Note that found is true when this
234				// isn't the last path element.
235				return
236			}
237			if !found {
238				// Didn't find the separator, so we're considering the last element.
239				break
240			}
241			p = rest
242		}
243
244		// Don't use GOROOT/src.
245		if reldir == "" && importPathRoot == "" {
246			return
247		}
248
249		name := path.Join(importPathRoot, filepath.ToSlash(reldir))
250		if !treeCanMatch(name) {
251			return
252		}
253
254		if !have[name] {
255			have[name] = true
256			if isMatch(name) {
257				if _, _, err := index.Package(reldir).ScanDir(tags); err != imports.ErrNoGo {
258					addPkg(name)
259				}
260			}
261		}
262	})
263}
264
265// MatchInModule identifies the packages matching the given pattern within the
266// given module version, which does not need to be in the build list or module
267// requirement graph.
268//
269// If m is the zero module.Version, MatchInModule matches the pattern
270// against the standard library (std and cmd) in GOROOT/src.
271func MatchInModule(ctx context.Context, pattern string, m module.Version, tags map[string]bool) *search.Match {
272	match := search.NewMatch(pattern)
273	if m == (module.Version{}) {
274		matchPackages(ctx, match, tags, includeStd, nil)
275	}
276
277	LoadModFile(ctx) // Sets Target, needed by fetch and matchPackages.
278
279	if !match.IsLiteral() {
280		matchPackages(ctx, match, tags, omitStd, []module.Version{m})
281		return match
282	}
283
284	root, isLocal, err := fetch(ctx, m)
285	if err != nil {
286		match.Errs = []error{err}
287		return match
288	}
289
290	dir, haveGoFiles, err := dirInModule(pattern, m.Path, root, isLocal)
291	if err != nil {
292		match.Errs = []error{err}
293		return match
294	}
295	if haveGoFiles {
296		if _, _, err := scanDir(root, dir, tags); err != imports.ErrNoGo {
297			// ErrNoGo indicates that the directory is not actually a Go package,
298			// perhaps due to the tags in use. Any other non-nil error indicates a
299			// problem with one or more of the Go source files, but such an error does
300			// not stop the package from existing, so it has no impact on matching.
301			match.Pkgs = []string{pattern}
302		}
303	}
304	return match
305}
306