github.com/octohelm/cuemod@v0.9.4/internal/cmd/go/internals/modload/search.go (about) 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 5 package modload 6 7 import ( 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 "github.com/octohelm/cuemod/internal/cmd/go/internals/cfg" 21 "github.com/octohelm/cuemod/internal/cmd/go/internals/fsys" 22 "github.com/octohelm/cuemod/internal/cmd/go/internals/gover" 23 "github.com/octohelm/cuemod/internal/cmd/go/internals/imports" 24 "github.com/octohelm/cuemod/internal/cmd/go/internals/modindex" 25 "github.com/octohelm/cuemod/internal/cmd/go/internals/par" 26 "github.com/octohelm/cuemod/internal/cmd/go/internals/search" 27 "github.com/octohelm/cuemod/internal/cmd/go/internals/str" 28 "github.com/octohelm/cuemod/internal/cmd/go/internals/trace" 29 "github.com/octohelm/cuemod/internal/cmd/internals/pkgpattern" 30 31 "golang.org/x/mod/module" 32 ) 33 34 type stdFilter int8 35 36 const ( 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. 44 func 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. 221 func 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. 271 func 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 }