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  }