cuelang.org/go@v0.10.1/internal/golangorgx/tools/imports/mod_cache.go (about) 1 // Copyright 2019 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 imports 6 7 import ( 8 "context" 9 "fmt" 10 "path" 11 "path/filepath" 12 "strings" 13 "sync" 14 15 "cuelang.org/go/internal/golangorgx/tools/gopathwalk" 16 "golang.org/x/mod/module" 17 ) 18 19 // To find packages to import, the resolver needs to know about all of 20 // the packages that could be imported. This includes packages that are 21 // already in modules that are in (1) the current module, (2) replace targets, 22 // and (3) packages in the module cache. Packages in (1) and (2) may change over 23 // time, as the client may edit the current module and locally replaced modules. 24 // The module cache (which includes all of the packages in (3)) can only 25 // ever be added to. 26 // 27 // The resolver can thus save state about packages in the module cache 28 // and guarantee that this will not change over time. To obtain information 29 // about new modules added to the module cache, the module cache should be 30 // rescanned. 31 // 32 // It is OK to serve information about modules that have been deleted, 33 // as they do still exist. 34 // TODO(suzmue): can we share information with the caller about 35 // what module needs to be downloaded to import this package? 36 37 type directoryPackageStatus int 38 39 const ( 40 _ directoryPackageStatus = iota 41 directoryScanned 42 nameLoaded 43 exportsLoaded 44 ) 45 46 // directoryPackageInfo holds (possibly incomplete) information about packages 47 // contained in a given directory. 48 type directoryPackageInfo struct { 49 // status indicates the extent to which this struct has been filled in. 50 status directoryPackageStatus 51 // err is non-nil when there was an error trying to reach status. 52 err error 53 54 // Set when status >= directoryScanned. 55 56 // dir is the absolute directory of this package. 57 dir string 58 rootType gopathwalk.RootType 59 // nonCanonicalImportPath is the package's expected import path. It may 60 // not actually be importable at that path. 61 nonCanonicalImportPath string 62 63 // Module-related information. 64 moduleDir string // The directory that is the module root of this dir. 65 moduleName string // The module name that contains this dir. 66 67 // Set when status >= nameLoaded. 68 69 packageName string // the package name, as declared in the source. 70 71 // Set when status >= exportsLoaded. 72 // TODO(rfindley): it's hard to see this, but exports depend implicitly on 73 // the default build context GOOS and GOARCH. 74 // 75 // We can make this explicit, and key exports by GOOS, GOARCH. 76 exports []string 77 } 78 79 // reachedStatus returns true when info has a status at least target and any error associated with 80 // an attempt to reach target. 81 func (info *directoryPackageInfo) reachedStatus(target directoryPackageStatus) (bool, error) { 82 if info.err == nil { 83 return info.status >= target, nil 84 } 85 if info.status == target { 86 return true, info.err 87 } 88 return true, nil 89 } 90 91 // DirInfoCache is a concurrency-safe map for storing information about 92 // directories that may contain packages. 93 // 94 // The information in this cache is built incrementally. Entries are initialized in scan. 95 // No new keys should be added in any other functions, as all directories containing 96 // packages are identified in scan. 97 // 98 // Other functions, including loadExports and findPackage, may update entries in this cache 99 // as they discover new things about the directory. 100 // 101 // The information in the cache is not expected to change for the cache's 102 // lifetime, so there is no protection against competing writes. Users should 103 // take care not to hold the cache across changes to the underlying files. 104 type DirInfoCache struct { 105 mu sync.Mutex 106 // dirs stores information about packages in directories, keyed by absolute path. 107 dirs map[string]*directoryPackageInfo 108 listeners map[*int]cacheListener 109 } 110 111 func NewDirInfoCache() *DirInfoCache { 112 return &DirInfoCache{ 113 dirs: make(map[string]*directoryPackageInfo), 114 listeners: make(map[*int]cacheListener), 115 } 116 } 117 118 type cacheListener func(directoryPackageInfo) 119 120 // ScanAndListen calls listener on all the items in the cache, and on anything 121 // newly added. The returned stop function waits for all in-flight callbacks to 122 // finish and blocks new ones. 123 func (d *DirInfoCache) ScanAndListen(ctx context.Context, listener cacheListener) func() { 124 ctx, cancel := context.WithCancel(ctx) 125 126 // Flushing out all the callbacks is tricky without knowing how many there 127 // are going to be. Setting an arbitrary limit makes it much easier. 128 const maxInFlight = 10 129 sema := make(chan struct{}, maxInFlight) 130 for i := 0; i < maxInFlight; i++ { 131 sema <- struct{}{} 132 } 133 134 cookie := new(int) // A unique ID we can use for the listener. 135 136 // We can't hold mu while calling the listener. 137 d.mu.Lock() 138 var keys []string 139 for key := range d.dirs { 140 keys = append(keys, key) 141 } 142 d.listeners[cookie] = func(info directoryPackageInfo) { 143 select { 144 case <-ctx.Done(): 145 return 146 case <-sema: 147 } 148 listener(info) 149 sema <- struct{}{} 150 } 151 d.mu.Unlock() 152 153 stop := func() { 154 cancel() 155 d.mu.Lock() 156 delete(d.listeners, cookie) 157 d.mu.Unlock() 158 for i := 0; i < maxInFlight; i++ { 159 <-sema 160 } 161 } 162 163 // Process the pre-existing keys. 164 for _, k := range keys { 165 select { 166 case <-ctx.Done(): 167 return stop 168 default: 169 } 170 if v, ok := d.Load(k); ok { 171 listener(v) 172 } 173 } 174 175 return stop 176 } 177 178 // Store stores the package info for dir. 179 func (d *DirInfoCache) Store(dir string, info directoryPackageInfo) { 180 d.mu.Lock() 181 // TODO(rfindley, golang/go#59216): should we overwrite an existing entry? 182 // That seems incorrect as the cache should be idempotent. 183 _, old := d.dirs[dir] 184 d.dirs[dir] = &info 185 var listeners []cacheListener 186 for _, l := range d.listeners { 187 listeners = append(listeners, l) 188 } 189 d.mu.Unlock() 190 191 if !old { 192 for _, l := range listeners { 193 l(info) 194 } 195 } 196 } 197 198 // Load returns a copy of the directoryPackageInfo for absolute directory dir. 199 func (d *DirInfoCache) Load(dir string) (directoryPackageInfo, bool) { 200 d.mu.Lock() 201 defer d.mu.Unlock() 202 info, ok := d.dirs[dir] 203 if !ok { 204 return directoryPackageInfo{}, false 205 } 206 return *info, true 207 } 208 209 // Keys returns the keys currently present in d. 210 func (d *DirInfoCache) Keys() (keys []string) { 211 d.mu.Lock() 212 defer d.mu.Unlock() 213 for key := range d.dirs { 214 keys = append(keys, key) 215 } 216 return keys 217 } 218 219 func (d *DirInfoCache) CachePackageName(info directoryPackageInfo) (string, error) { 220 if loaded, err := info.reachedStatus(nameLoaded); loaded { 221 return info.packageName, err 222 } 223 if scanned, err := info.reachedStatus(directoryScanned); !scanned || err != nil { 224 return "", fmt.Errorf("cannot read package name, scan error: %v", err) 225 } 226 info.packageName, info.err = packageDirToName(info.dir) 227 info.status = nameLoaded 228 d.Store(info.dir, info) 229 return info.packageName, info.err 230 } 231 232 func (d *DirInfoCache) CacheExports(ctx context.Context, env *ProcessEnv, info directoryPackageInfo) (string, []string, error) { 233 if reached, _ := info.reachedStatus(exportsLoaded); reached { 234 return info.packageName, info.exports, info.err 235 } 236 if reached, err := info.reachedStatus(nameLoaded); reached && err != nil { 237 return "", nil, err 238 } 239 info.packageName, info.exports, info.err = loadExportsFromFiles(ctx, env, info.dir, false) 240 if info.err == context.Canceled || info.err == context.DeadlineExceeded { 241 return info.packageName, info.exports, info.err 242 } 243 // The cache structure wants things to proceed linearly. We can skip a 244 // step here, but only if we succeed. 245 if info.status == nameLoaded || info.err == nil { 246 info.status = exportsLoaded 247 } else { 248 info.status = nameLoaded 249 } 250 d.Store(info.dir, info) 251 return info.packageName, info.exports, info.err 252 } 253 254 // ScanModuleCache walks the given directory, which must be a GOMODCACHE value, 255 // for directory package information, storing the results in cache. 256 func ScanModuleCache(dir string, cache *DirInfoCache, logf func(string, ...any)) { 257 // Note(rfindley): it's hard to see, but this function attempts to implement 258 // just the side effects on cache of calling PrimeCache with a ProcessEnv 259 // that has the given dir as its GOMODCACHE. 260 // 261 // Teasing out the control flow, we see that we can avoid any handling of 262 // vendor/ and can infer module info entirely from the path, simplifying the 263 // logic here. 264 265 root := gopathwalk.Root{ 266 Path: filepath.Clean(dir), 267 Type: gopathwalk.RootModuleCache, 268 } 269 270 directoryInfo := func(root gopathwalk.Root, dir string) directoryPackageInfo { 271 // This is a copy of ModuleResolver.scanDirForPackage, trimmed down to 272 // logic that applies to a module cache directory. 273 274 subdir := "" 275 if dir != root.Path { 276 subdir = dir[len(root.Path)+len("/"):] 277 } 278 279 matches := modCacheRegexp.FindStringSubmatch(subdir) 280 if len(matches) == 0 { 281 return directoryPackageInfo{ 282 status: directoryScanned, 283 err: fmt.Errorf("invalid module cache path: %v", subdir), 284 } 285 } 286 modPath, err := module.UnescapePath(filepath.ToSlash(matches[1])) 287 if err != nil { 288 if logf != nil { 289 logf("decoding module cache path %q: %v", subdir, err) 290 } 291 return directoryPackageInfo{ 292 status: directoryScanned, 293 err: fmt.Errorf("decoding module cache path %q: %v", subdir, err), 294 } 295 } 296 importPath := path.Join(modPath, filepath.ToSlash(matches[3])) 297 index := strings.Index(dir, matches[1]+"@"+matches[2]) 298 modDir := filepath.Join(dir[:index], matches[1]+"@"+matches[2]) 299 modName := readModName(filepath.Join(modDir, "go.mod")) 300 return directoryPackageInfo{ 301 status: directoryScanned, 302 dir: dir, 303 rootType: root.Type, 304 nonCanonicalImportPath: importPath, 305 moduleDir: modDir, 306 moduleName: modName, 307 } 308 } 309 310 add := func(root gopathwalk.Root, dir string) { 311 info := directoryInfo(root, dir) 312 cache.Store(info.dir, info) 313 } 314 315 skip := func(_ gopathwalk.Root, dir string) bool { 316 // Skip directories that have already been scanned. 317 // 318 // Note that gopathwalk only adds "package" directories, which must contain 319 // a .go file, and all such package directories in the module cache are 320 // immutable. So if we can load a dir, it can be skipped. 321 info, ok := cache.Load(dir) 322 if !ok { 323 return false 324 } 325 packageScanned, _ := info.reachedStatus(directoryScanned) 326 return packageScanned 327 } 328 329 gopathwalk.WalkSkip([]gopathwalk.Root{root}, add, skip, gopathwalk.Options{Logf: logf, ModulesEnabled: true}) 330 }