blob: d392b5bf3dd9780ec02e2d0a8d96adff7e7254c5 [file] [log] [blame]
// 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 modload
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"cmd/go/internal/cfg"
"cmd/go/internal/fsys"
"cmd/go/internal/gover"
"cmd/go/internal/imports"
"cmd/go/internal/modindex"
"cmd/go/internal/par"
"cmd/go/internal/search"
"cmd/go/internal/str"
"cmd/go/internal/trace"
"cmd/internal/pkgpattern"
"golang.org/x/mod/module"
)
type stdFilter int8
const (
omitStd = stdFilter(iota)
includeStd
)
// matchPackages is like m.MatchPackages, but uses a local variable (rather than
// a global) for tags, can include or exclude packages in the standard library,
// and is restricted to the given list of modules.
func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, filter stdFilter, modules []module.Version) {
ctx, span := trace.StartSpan(ctx, "modload.matchPackages")
defer span.Done()
m.Pkgs = []string{}
isMatch := func(string) bool { return true }
treeCanMatch := func(string) bool { return true }
if !m.IsMeta() {
isMatch = pkgpattern.MatchPattern(m.Pattern())
treeCanMatch = pkgpattern.TreeCanMatchPattern(m.Pattern())
}
var mu sync.Mutex
have := map[string]bool{
"builtin": true, // ignore pseudo-package that exists only for documentation
}
addPkg := func(p string) {
mu.Lock()
m.Pkgs = append(m.Pkgs, p)
mu.Unlock()
}
if !cfg.BuildContext.CgoEnabled {
have["runtime/cgo"] = true // ignore during walk
}
type pruning int8
const (
pruneVendor = pruning(1 << iota)
pruneGoMod
)
q := par.NewQueue(runtime.GOMAXPROCS(0))
walkPkgs := func(root, importPathRoot string, prune pruning) {
_, span := trace.StartSpan(ctx, "walkPkgs "+root)
defer span.Done()
// If the root itself is a symlink to a directory,
// we want to follow it (see https://go.dev/issue/50807).
// Add a trailing separator to force that to happen.
root = str.WithFilePathSeparator(filepath.Clean(root))
err := fsys.Walk(root, func(pkgDir string, fi fs.FileInfo, err error) error {
if err != nil {
m.AddError(err)
return nil
}
want := true
elem := ""
// Don't use GOROOT/src but do walk down into it.
if pkgDir == root {
if importPathRoot == "" {
return nil
}
} else {
// Avoid .foo, _foo, and testdata subdirectory trees.
_, elem = filepath.Split(pkgDir)
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
want = false
}
}
name := path.Join(importPathRoot, filepath.ToSlash(pkgDir[len(root):]))
if !treeCanMatch(name) {
want = false
}
if !fi.IsDir() {
if fi.Mode()&fs.ModeSymlink != 0 && want && strings.Contains(m.Pattern(), "...") {
if target, err := fsys.Stat(pkgDir); err == nil && target.IsDir() {
fmt.Fprintf(os.Stderr, "warning: ignoring symlink %s\n", pkgDir)
}
}
return nil
}
if !want {
return filepath.SkipDir
}
// Stop at module boundaries.
if (prune&pruneGoMod != 0) && pkgDir != root {
if fi, err := os.Stat(filepath.Join(pkgDir, "go.mod")); err == nil && !fi.IsDir() {
return filepath.SkipDir
}
}
if !have[name] {
have[name] = true
if isMatch(name) {
q.Add(func() {
if _, _, err := scanDir(root, pkgDir, tags); err != imports.ErrNoGo {
addPkg(name)
}
})
}
}
if elem == "vendor" && (prune&pruneVendor != 0) {
return filepath.SkipDir
}
return nil
})
if err != nil {
m.AddError(err)
}
}
// Wait for all in-flight operations to complete before returning.
defer func() {
<-q.Idle()
sort.Strings(m.Pkgs) // sort everything we added for determinism
}()
if filter == includeStd {
walkPkgs(cfg.GOROOTsrc, "", pruneGoMod)
if treeCanMatch("cmd") {
walkPkgs(filepath.Join(cfg.GOROOTsrc, "cmd"), "cmd", pruneGoMod)
}
}
if cfg.BuildMod == "vendor" {
for _, mod := range MainModules.Versions() {
if modRoot := MainModules.ModRoot(mod); modRoot != "" {
walkPkgs(modRoot, MainModules.PathPrefix(mod), pruneGoMod|pruneVendor)
}
}
if HasModRoot() {
walkPkgs(VendorDir(), "", pruneVendor)
}
return
}
for _, mod := range modules {
if gover.IsToolchain(mod.Path) || !treeCanMatch(mod.Path) {
continue
}
var (
root, modPrefix string
isLocal bool
)
if MainModules.Contains(mod.Path) {
if MainModules.ModRoot(mod) == "" {
continue // If there is no main module, we can't search in it.
}
root = MainModules.ModRoot(mod)
modPrefix = MainModules.PathPrefix(mod)
isLocal = true
} else {
var err error
root, isLocal, err = fetch(ctx, mod)
if err != nil {
m.AddError(err)
continue
}
modPrefix = mod.Path
}
if mi, err := modindex.GetModule(root); err == nil {
walkFromIndex(mi, modPrefix, isMatch, treeCanMatch, tags, have, addPkg)
continue
} else if !errors.Is(err, modindex.ErrNotIndexed) {
m.AddError(err)
}
prune := pruneVendor
if isLocal {
prune |= pruneGoMod
}
walkPkgs(root, modPrefix, prune)
}
}
// walkFromIndex matches packages in a module using the module index. modroot
// is the module's root directory on disk, index is the modindex.Module for the
// module, and importPathRoot is the module's path prefix.
func walkFromIndex(index *modindex.Module, importPathRoot string, isMatch, treeCanMatch func(string) bool, tags, have map[string]bool, addPkg func(string)) {
index.Walk(func(reldir string) {
// Avoid .foo, _foo, and testdata subdirectory trees.
p := reldir
for {
elem, rest, found := strings.Cut(p, string(filepath.Separator))
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
return
}
if found && elem == "vendor" {
// Ignore this path if it contains the element "vendor" anywhere
// except for the last element (packages named vendor are allowed
// for historical reasons). Note that found is true when this
// isn't the last path element.
return
}
if !found {
// Didn't find the separator, so we're considering the last element.
break
}
p = rest
}
// Don't use GOROOT/src.
if reldir == "" && importPathRoot == "" {
return
}
name := path.Join(importPathRoot, filepath.ToSlash(reldir))
if !treeCanMatch(name) {
return
}
if !have[name] {
have[name] = true
if isMatch(name) {
if _, _, err := index.Package(reldir).ScanDir(tags); err != imports.ErrNoGo {
addPkg(name)
}
}
}
})
}
// MatchInModule identifies the packages matching the given pattern within the
// given module version, which does not need to be in the build list or module
// requirement graph.
//
// If m is the zero module.Version, MatchInModule matches the pattern
// against the standard library (std and cmd) in GOROOT/src.
func MatchInModule(ctx context.Context, pattern string, m module.Version, tags map[string]bool) *search.Match {
match := search.NewMatch(pattern)
if m == (module.Version{}) {
matchPackages(ctx, match, tags, includeStd, nil)
}
LoadModFile(ctx) // Sets Target, needed by fetch and matchPackages.
if !match.IsLiteral() {
matchPackages(ctx, match, tags, omitStd, []module.Version{m})
return match
}
root, isLocal, err := fetch(ctx, m)
if err != nil {
match.Errs = []error{err}
return match
}
dir, haveGoFiles, err := dirInModule(pattern, m.Path, root, isLocal)
if err != nil {
match.Errs = []error{err}
return match
}
if haveGoFiles {
if _, _, err := scanDir(root, dir, tags); err != imports.ErrNoGo {
// ErrNoGo indicates that the directory is not actually a Go package,
// perhaps due to the tags in use. Any other non-nil error indicates a
// problem with one or more of the Go source files, but such an error does
// not stop the package from existing, so it has no impact on matching.
match.Pkgs = []string{pattern}
}
}
return match
}