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