Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/go/internal/modfetch/cache.go

Documentation: cmd/go/internal/modfetch

     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 modfetch
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/fs"
    14  	"math/rand"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  
    21  	"cmd/go/internal/base"
    22  	"cmd/go/internal/cfg"
    23  	"cmd/go/internal/lockedfile"
    24  	"cmd/go/internal/modfetch/codehost"
    25  	"cmd/go/internal/par"
    26  	"cmd/go/internal/robustio"
    27  
    28  	"golang.org/x/mod/module"
    29  	"golang.org/x/mod/semver"
    30  )
    31  
    32  func cacheDir(path string) (string, error) {
    33  	if err := checkCacheDir(); err != nil {
    34  		return "", err
    35  	}
    36  	enc, err := module.EscapePath(path)
    37  	if err != nil {
    38  		return "", err
    39  	}
    40  	return filepath.Join(cfg.GOMODCACHE, "cache/download", enc, "/@v"), nil
    41  }
    42  
    43  func CachePath(m module.Version, suffix string) (string, error) {
    44  	dir, err := cacheDir(m.Path)
    45  	if err != nil {
    46  		return "", err
    47  	}
    48  	if !semver.IsValid(m.Version) {
    49  		return "", fmt.Errorf("non-semver module version %q", m.Version)
    50  	}
    51  	if module.CanonicalVersion(m.Version) != m.Version {
    52  		return "", fmt.Errorf("non-canonical module version %q", m.Version)
    53  	}
    54  	encVer, err := module.EscapeVersion(m.Version)
    55  	if err != nil {
    56  		return "", err
    57  	}
    58  	return filepath.Join(dir, encVer+"."+suffix), nil
    59  }
    60  
    61  // DownloadDir returns the directory to which m should have been downloaded.
    62  // An error will be returned if the module path or version cannot be escaped.
    63  // An error satisfying errors.Is(err, fs.ErrNotExist) will be returned
    64  // along with the directory if the directory does not exist or if the directory
    65  // is not completely populated.
    66  func DownloadDir(m module.Version) (string, error) {
    67  	if err := checkCacheDir(); err != nil {
    68  		return "", err
    69  	}
    70  	enc, err := module.EscapePath(m.Path)
    71  	if err != nil {
    72  		return "", err
    73  	}
    74  	if !semver.IsValid(m.Version) {
    75  		return "", fmt.Errorf("non-semver module version %q", m.Version)
    76  	}
    77  	if module.CanonicalVersion(m.Version) != m.Version {
    78  		return "", fmt.Errorf("non-canonical module version %q", m.Version)
    79  	}
    80  	encVer, err := module.EscapeVersion(m.Version)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  
    85  	// Check whether the directory itself exists.
    86  	dir := filepath.Join(cfg.GOMODCACHE, enc+"@"+encVer)
    87  	if fi, err := os.Stat(dir); os.IsNotExist(err) {
    88  		return dir, err
    89  	} else if err != nil {
    90  		return dir, &DownloadDirPartialError{dir, err}
    91  	} else if !fi.IsDir() {
    92  		return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
    93  	}
    94  
    95  	// Check if a .partial file exists. This is created at the beginning of
    96  	// a download and removed after the zip is extracted.
    97  	partialPath, err := CachePath(m, "partial")
    98  	if err != nil {
    99  		return dir, err
   100  	}
   101  	if _, err := os.Stat(partialPath); err == nil {
   102  		return dir, &DownloadDirPartialError{dir, errors.New("not completely extracted")}
   103  	} else if !os.IsNotExist(err) {
   104  		return dir, err
   105  	}
   106  
   107  	// Check if a .ziphash file exists. It should be created before the
   108  	// zip is extracted, but if it was deleted (by another program?), we need
   109  	// to re-calculate it. Note that checkMod will repopulate the ziphash
   110  	// file if it doesn't exist, but if the module is excluded by checks
   111  	// through GONOSUMDB or GOPRIVATE, that check and repopulation won't happen.
   112  	ziphashPath, err := CachePath(m, "ziphash")
   113  	if err != nil {
   114  		return dir, err
   115  	}
   116  	if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
   117  		return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
   118  	} else if err != nil {
   119  		return dir, err
   120  	}
   121  	return dir, nil
   122  }
   123  
   124  // DownloadDirPartialError is returned by DownloadDir if a module directory
   125  // exists but was not completely populated.
   126  //
   127  // DownloadDirPartialError is equivalent to fs.ErrNotExist.
   128  type DownloadDirPartialError struct {
   129  	Dir string
   130  	Err error
   131  }
   132  
   133  func (e *DownloadDirPartialError) Error() string     { return fmt.Sprintf("%s: %v", e.Dir, e.Err) }
   134  func (e *DownloadDirPartialError) Is(err error) bool { return err == fs.ErrNotExist }
   135  
   136  // lockVersion locks a file within the module cache that guards the downloading
   137  // and extraction of the zipfile for the given module version.
   138  func lockVersion(mod module.Version) (unlock func(), err error) {
   139  	path, err := CachePath(mod, "lock")
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
   144  		return nil, err
   145  	}
   146  	return lockedfile.MutexAt(path).Lock()
   147  }
   148  
   149  // SideLock locks a file within the module cache that previously guarded
   150  // edits to files outside the cache, such as go.sum and go.mod files in the
   151  // user's working directory.
   152  // If err is nil, the caller MUST eventually call the unlock function.
   153  func SideLock() (unlock func(), err error) {
   154  	if err := checkCacheDir(); err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	path := filepath.Join(cfg.GOMODCACHE, "cache", "lock")
   159  	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
   160  		return nil, fmt.Errorf("failed to create cache directory: %w", err)
   161  	}
   162  
   163  	return lockedfile.MutexAt(path).Lock()
   164  }
   165  
   166  // A cachingRepo is a cache around an underlying Repo,
   167  // avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
   168  // It is also safe for simultaneous use by multiple goroutines
   169  // (so that it can be returned from Lookup multiple times).
   170  // It serializes calls to the underlying Repo.
   171  type cachingRepo struct {
   172  	path  string
   173  	cache par.Cache // cache for all operations
   174  
   175  	once     sync.Once
   176  	initRepo func() (Repo, error)
   177  	r        Repo
   178  }
   179  
   180  func newCachingRepo(path string, initRepo func() (Repo, error)) *cachingRepo {
   181  	return &cachingRepo{
   182  		path:     path,
   183  		initRepo: initRepo,
   184  	}
   185  }
   186  
   187  func (r *cachingRepo) repo() Repo {
   188  	r.once.Do(func() {
   189  		var err error
   190  		r.r, err = r.initRepo()
   191  		if err != nil {
   192  			r.r = errRepo{r.path, err}
   193  		}
   194  	})
   195  	return r.r
   196  }
   197  
   198  func (r *cachingRepo) ModulePath() string {
   199  	return r.path
   200  }
   201  
   202  func (r *cachingRepo) Versions(prefix string) ([]string, error) {
   203  	type cached struct {
   204  		list []string
   205  		err  error
   206  	}
   207  	c := r.cache.Do("versions:"+prefix, func() interface{} {
   208  		list, err := r.repo().Versions(prefix)
   209  		return cached{list, err}
   210  	}).(cached)
   211  
   212  	if c.err != nil {
   213  		return nil, c.err
   214  	}
   215  	return append([]string(nil), c.list...), nil
   216  }
   217  
   218  type cachedInfo struct {
   219  	info *RevInfo
   220  	err  error
   221  }
   222  
   223  func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
   224  	c := r.cache.Do("stat:"+rev, func() interface{} {
   225  		file, info, err := readDiskStat(r.path, rev)
   226  		if err == nil {
   227  			return cachedInfo{info, nil}
   228  		}
   229  
   230  		info, err = r.repo().Stat(rev)
   231  		if err == nil {
   232  			// If we resolved, say, 1234abcde to v0.0.0-20180604122334-1234abcdef78,
   233  			// then save the information under the proper version, for future use.
   234  			if info.Version != rev {
   235  				file, _ = CachePath(module.Version{Path: r.path, Version: info.Version}, "info")
   236  				r.cache.Do("stat:"+info.Version, func() interface{} {
   237  					return cachedInfo{info, err}
   238  				})
   239  			}
   240  
   241  			if err := writeDiskStat(file, info); err != nil {
   242  				fmt.Fprintf(os.Stderr, "go: writing stat cache: %v\n", err)
   243  			}
   244  		}
   245  		return cachedInfo{info, err}
   246  	}).(cachedInfo)
   247  
   248  	if c.err != nil {
   249  		return nil, c.err
   250  	}
   251  	info := *c.info
   252  	return &info, nil
   253  }
   254  
   255  func (r *cachingRepo) Latest() (*RevInfo, error) {
   256  	c := r.cache.Do("latest:", func() interface{} {
   257  		info, err := r.repo().Latest()
   258  
   259  		// Save info for likely future Stat call.
   260  		if err == nil {
   261  			r.cache.Do("stat:"+info.Version, func() interface{} {
   262  				return cachedInfo{info, err}
   263  			})
   264  			if file, _, err := readDiskStat(r.path, info.Version); err != nil {
   265  				writeDiskStat(file, info)
   266  			}
   267  		}
   268  
   269  		return cachedInfo{info, err}
   270  	}).(cachedInfo)
   271  
   272  	if c.err != nil {
   273  		return nil, c.err
   274  	}
   275  	info := *c.info
   276  	return &info, nil
   277  }
   278  
   279  func (r *cachingRepo) GoMod(version string) ([]byte, error) {
   280  	type cached struct {
   281  		text []byte
   282  		err  error
   283  	}
   284  	c := r.cache.Do("gomod:"+version, func() interface{} {
   285  		file, text, err := readDiskGoMod(r.path, version)
   286  		if err == nil {
   287  			// Note: readDiskGoMod already called checkGoMod.
   288  			return cached{text, nil}
   289  		}
   290  
   291  		text, err = r.repo().GoMod(version)
   292  		if err == nil {
   293  			if err := checkGoMod(r.path, version, text); err != nil {
   294  				return cached{text, err}
   295  			}
   296  			if err := writeDiskGoMod(file, text); err != nil {
   297  				fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
   298  			}
   299  		}
   300  		return cached{text, err}
   301  	}).(cached)
   302  
   303  	if c.err != nil {
   304  		return nil, c.err
   305  	}
   306  	return append([]byte(nil), c.text...), nil
   307  }
   308  
   309  func (r *cachingRepo) Zip(dst io.Writer, version string) error {
   310  	return r.repo().Zip(dst, version)
   311  }
   312  
   313  // InfoFile is like Lookup(path).Stat(version) but returns the name of the file
   314  // containing the cached information.
   315  func InfoFile(path, version string) (string, error) {
   316  	if !semver.IsValid(version) {
   317  		return "", fmt.Errorf("invalid version %q", version)
   318  	}
   319  
   320  	if file, _, err := readDiskStat(path, version); err == nil {
   321  		return file, nil
   322  	}
   323  
   324  	err := TryProxies(func(proxy string) error {
   325  		_, err := Lookup(proxy, path).Stat(version)
   326  		return err
   327  	})
   328  	if err != nil {
   329  		return "", err
   330  	}
   331  
   332  	// Stat should have populated the disk cache for us.
   333  	file, err := CachePath(module.Version{Path: path, Version: version}, "info")
   334  	if err != nil {
   335  		return "", err
   336  	}
   337  	return file, nil
   338  }
   339  
   340  // GoMod is like Lookup(path).GoMod(rev) but avoids the
   341  // repository path resolution in Lookup if the result is
   342  // already cached on local disk.
   343  func GoMod(path, rev string) ([]byte, error) {
   344  	// Convert commit hash to pseudo-version
   345  	// to increase cache hit rate.
   346  	if !semver.IsValid(rev) {
   347  		if _, info, err := readDiskStat(path, rev); err == nil {
   348  			rev = info.Version
   349  		} else {
   350  			if errors.Is(err, statCacheErr) {
   351  				return nil, err
   352  			}
   353  			err := TryProxies(func(proxy string) error {
   354  				info, err := Lookup(proxy, path).Stat(rev)
   355  				if err == nil {
   356  					rev = info.Version
   357  				}
   358  				return err
   359  			})
   360  			if err != nil {
   361  				return nil, err
   362  			}
   363  		}
   364  	}
   365  
   366  	_, data, err := readDiskGoMod(path, rev)
   367  	if err == nil {
   368  		return data, nil
   369  	}
   370  
   371  	err = TryProxies(func(proxy string) (err error) {
   372  		data, err = Lookup(proxy, path).GoMod(rev)
   373  		return err
   374  	})
   375  	return data, err
   376  }
   377  
   378  // GoModFile is like GoMod but returns the name of the file containing
   379  // the cached information.
   380  func GoModFile(path, version string) (string, error) {
   381  	if !semver.IsValid(version) {
   382  		return "", fmt.Errorf("invalid version %q", version)
   383  	}
   384  	if _, err := GoMod(path, version); err != nil {
   385  		return "", err
   386  	}
   387  	// GoMod should have populated the disk cache for us.
   388  	file, err := CachePath(module.Version{Path: path, Version: version}, "mod")
   389  	if err != nil {
   390  		return "", err
   391  	}
   392  	return file, nil
   393  }
   394  
   395  // GoModSum returns the go.sum entry for the module version's go.mod file.
   396  // (That is, it returns the entry listed in go.sum as "path version/go.mod".)
   397  func GoModSum(path, version string) (string, error) {
   398  	if !semver.IsValid(version) {
   399  		return "", fmt.Errorf("invalid version %q", version)
   400  	}
   401  	data, err := GoMod(path, version)
   402  	if err != nil {
   403  		return "", err
   404  	}
   405  	sum, err := goModSum(data)
   406  	if err != nil {
   407  		return "", err
   408  	}
   409  	return sum, nil
   410  }
   411  
   412  var errNotCached = fmt.Errorf("not in cache")
   413  
   414  // readDiskStat reads a cached stat result from disk,
   415  // returning the name of the cache file and the result.
   416  // If the read fails, the caller can use
   417  // writeDiskStat(file, info) to write a new cache entry.
   418  func readDiskStat(path, rev string) (file string, info *RevInfo, err error) {
   419  	file, data, err := readDiskCache(path, rev, "info")
   420  	if err != nil {
   421  		// If the cache already contains a pseudo-version with the given hash, we
   422  		// would previously return that pseudo-version without checking upstream.
   423  		// However, that produced an unfortunate side-effect: if the author added a
   424  		// tag to the repository, 'go get' would not pick up the effect of that new
   425  		// tag on the existing commits, and 'go' commands that referred to those
   426  		// commits would use the previous name instead of the new one.
   427  		//
   428  		// That's especially problematic if the original pseudo-version starts with
   429  		// v0.0.0-, as was the case for all pseudo-versions during vgo development,
   430  		// since a v0.0.0- pseudo-version has lower precedence than pretty much any
   431  		// tagged version.
   432  		//
   433  		// In practice, we're only looking up by hash during initial conversion of a
   434  		// legacy config and during an explicit 'go get', and a little extra latency
   435  		// for those operations seems worth the benefit of picking up more accurate
   436  		// versions.
   437  		//
   438  		// Fall back to this resolution scheme only if the GOPROXY setting prohibits
   439  		// us from resolving upstream tags.
   440  		if cfg.GOPROXY == "off" {
   441  			if file, info, err := readDiskStatByHash(path, rev); err == nil {
   442  				return file, info, nil
   443  			}
   444  		}
   445  		return file, nil, err
   446  	}
   447  	info = new(RevInfo)
   448  	if err := json.Unmarshal(data, info); err != nil {
   449  		return file, nil, errNotCached
   450  	}
   451  	// The disk might have stale .info files that have Name and Short fields set.
   452  	// We want to canonicalize to .info files with those fields omitted.
   453  	// Remarshal and update the cache file if needed.
   454  	data2, err := json.Marshal(info)
   455  	if err == nil && !bytes.Equal(data2, data) {
   456  		writeDiskCache(file, data)
   457  	}
   458  	return file, info, nil
   459  }
   460  
   461  // readDiskStatByHash is a fallback for readDiskStat for the case
   462  // where rev is a commit hash instead of a proper semantic version.
   463  // In that case, we look for a cached pseudo-version that matches
   464  // the commit hash. If we find one, we use it.
   465  // This matters most for converting legacy package management
   466  // configs, when we are often looking up commits by full hash.
   467  // Without this check we'd be doing network I/O to the remote repo
   468  // just to find out about a commit we already know about
   469  // (and have cached under its pseudo-version).
   470  func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error) {
   471  	if cfg.GOMODCACHE == "" {
   472  		// Do not download to current directory.
   473  		return "", nil, errNotCached
   474  	}
   475  
   476  	if !codehost.AllHex(rev) || len(rev) < 12 {
   477  		return "", nil, errNotCached
   478  	}
   479  	rev = rev[:12]
   480  	cdir, err := cacheDir(path)
   481  	if err != nil {
   482  		return "", nil, errNotCached
   483  	}
   484  	dir, err := os.Open(cdir)
   485  	if err != nil {
   486  		return "", nil, errNotCached
   487  	}
   488  	names, err := dir.Readdirnames(-1)
   489  	dir.Close()
   490  	if err != nil {
   491  		return "", nil, errNotCached
   492  	}
   493  
   494  	// A given commit hash may map to more than one pseudo-version,
   495  	// depending on which tags are present on the repository.
   496  	// Take the highest such version.
   497  	var maxVersion string
   498  	suffix := "-" + rev + ".info"
   499  	err = errNotCached
   500  	for _, name := range names {
   501  		if strings.HasSuffix(name, suffix) {
   502  			v := strings.TrimSuffix(name, ".info")
   503  			if module.IsPseudoVersion(v) && semver.Compare(v, maxVersion) > 0 {
   504  				maxVersion = v
   505  				file, info, err = readDiskStat(path, strings.TrimSuffix(name, ".info"))
   506  			}
   507  		}
   508  	}
   509  	return file, info, err
   510  }
   511  
   512  // oldVgoPrefix is the prefix in the old auto-generated cached go.mod files.
   513  // We stopped trying to auto-generate the go.mod files. Now we use a trivial
   514  // go.mod with only a module line, and we've dropped the version prefix
   515  // entirely. If we see a version prefix, that means we're looking at an old copy
   516  // and should ignore it.
   517  var oldVgoPrefix = []byte("//vgo 0.0.")
   518  
   519  // readDiskGoMod reads a cached go.mod file from disk,
   520  // returning the name of the cache file and the result.
   521  // If the read fails, the caller can use
   522  // writeDiskGoMod(file, data) to write a new cache entry.
   523  func readDiskGoMod(path, rev string) (file string, data []byte, err error) {
   524  	file, data, err = readDiskCache(path, rev, "mod")
   525  
   526  	// If the file has an old auto-conversion prefix, pretend it's not there.
   527  	if bytes.HasPrefix(data, oldVgoPrefix) {
   528  		err = errNotCached
   529  		data = nil
   530  	}
   531  
   532  	if err == nil {
   533  		if err := checkGoMod(path, rev, data); err != nil {
   534  			return "", nil, err
   535  		}
   536  	}
   537  
   538  	return file, data, err
   539  }
   540  
   541  // readDiskCache is the generic "read from a cache file" implementation.
   542  // It takes the revision and an identifying suffix for the kind of data being cached.
   543  // It returns the name of the cache file and the content of the file.
   544  // If the read fails, the caller can use
   545  // writeDiskCache(file, data) to write a new cache entry.
   546  func readDiskCache(path, rev, suffix string) (file string, data []byte, err error) {
   547  	file, err = CachePath(module.Version{Path: path, Version: rev}, suffix)
   548  	if err != nil {
   549  		return "", nil, errNotCached
   550  	}
   551  	data, err = robustio.ReadFile(file)
   552  	if err != nil {
   553  		return file, nil, errNotCached
   554  	}
   555  	return file, data, nil
   556  }
   557  
   558  // writeDiskStat writes a stat result cache entry.
   559  // The file name must have been returned by a previous call to readDiskStat.
   560  func writeDiskStat(file string, info *RevInfo) error {
   561  	if file == "" {
   562  		return nil
   563  	}
   564  	js, err := json.Marshal(info)
   565  	if err != nil {
   566  		return err
   567  	}
   568  	return writeDiskCache(file, js)
   569  }
   570  
   571  // writeDiskGoMod writes a go.mod cache entry.
   572  // The file name must have been returned by a previous call to readDiskGoMod.
   573  func writeDiskGoMod(file string, text []byte) error {
   574  	return writeDiskCache(file, text)
   575  }
   576  
   577  // writeDiskCache is the generic "write to a cache file" implementation.
   578  // The file must have been returned by a previous call to readDiskCache.
   579  func writeDiskCache(file string, data []byte) error {
   580  	if file == "" {
   581  		return nil
   582  	}
   583  	// Make sure directory for file exists.
   584  	if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
   585  		return err
   586  	}
   587  
   588  	// Write the file to a temporary location, and then rename it to its final
   589  	// path to reduce the likelihood of a corrupt file existing at that final path.
   590  	f, err := tempFile(filepath.Dir(file), filepath.Base(file), 0666)
   591  	if err != nil {
   592  		return err
   593  	}
   594  	defer func() {
   595  		// Only call os.Remove on f.Name() if we failed to rename it: otherwise,
   596  		// some other process may have created a new file with the same name after
   597  		// the rename completed.
   598  		if err != nil {
   599  			f.Close()
   600  			os.Remove(f.Name())
   601  		}
   602  	}()
   603  
   604  	if _, err := f.Write(data); err != nil {
   605  		return err
   606  	}
   607  	if err := f.Close(); err != nil {
   608  		return err
   609  	}
   610  	if err := robustio.Rename(f.Name(), file); err != nil {
   611  		return err
   612  	}
   613  
   614  	if strings.HasSuffix(file, ".mod") {
   615  		rewriteVersionList(filepath.Dir(file))
   616  	}
   617  	return nil
   618  }
   619  
   620  // tempFile creates a new temporary file with given permission bits.
   621  func tempFile(dir, prefix string, perm fs.FileMode) (f *os.File, err error) {
   622  	for i := 0; i < 10000; i++ {
   623  		name := filepath.Join(dir, prefix+strconv.Itoa(rand.Intn(1000000000))+".tmp")
   624  		f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, perm)
   625  		if os.IsExist(err) {
   626  			continue
   627  		}
   628  		break
   629  	}
   630  	return
   631  }
   632  
   633  // rewriteVersionList rewrites the version list in dir
   634  // after a new *.mod file has been written.
   635  func rewriteVersionList(dir string) (err error) {
   636  	if filepath.Base(dir) != "@v" {
   637  		base.Fatalf("go: internal error: misuse of rewriteVersionList")
   638  	}
   639  
   640  	listFile := filepath.Join(dir, "list")
   641  
   642  	// Lock listfile when writing to it to try to avoid corruption to the file.
   643  	// Under rare circumstances, for instance, if the system loses power in the
   644  	// middle of a write it is possible for corrupt data to be written. This is
   645  	// not a problem for the go command itself, but may be an issue if the the
   646  	// cache is being served by a GOPROXY HTTP server. This will be corrected
   647  	// the next time a new version of the module is fetched and the file is rewritten.
   648  	// TODO(matloob): golang.org/issue/43313 covers adding a go mod verify
   649  	// command that removes module versions that fail checksums. It should also
   650  	// remove list files that are detected to be corrupt.
   651  	f, err := lockedfile.Edit(listFile)
   652  	if err != nil {
   653  		return err
   654  	}
   655  	defer func() {
   656  		if cerr := f.Close(); cerr != nil && err == nil {
   657  			err = cerr
   658  		}
   659  	}()
   660  	infos, err := os.ReadDir(dir)
   661  	if err != nil {
   662  		return err
   663  	}
   664  	var list []string
   665  	for _, info := range infos {
   666  		// We look for *.mod files on the theory that if we can't supply
   667  		// the .mod file then there's no point in listing that version,
   668  		// since it's unusable. (We can have *.info without *.mod.)
   669  		// We don't require *.zip files on the theory that for code only
   670  		// involved in module graph construction, many *.zip files
   671  		// will never be requested.
   672  		name := info.Name()
   673  		if strings.HasSuffix(name, ".mod") {
   674  			v := strings.TrimSuffix(name, ".mod")
   675  			if v != "" && module.CanonicalVersion(v) == v {
   676  				list = append(list, v)
   677  			}
   678  		}
   679  	}
   680  	semver.Sort(list)
   681  
   682  	var buf bytes.Buffer
   683  	for _, v := range list {
   684  		buf.WriteString(v)
   685  		buf.WriteString("\n")
   686  	}
   687  	if fi, err := f.Stat(); err == nil && int(fi.Size()) == buf.Len() {
   688  		old := make([]byte, buf.Len()+1)
   689  		if n, err := f.ReadAt(old, 0); err == io.EOF && n == buf.Len() && bytes.Equal(buf.Bytes(), old) {
   690  			return nil // No edit needed.
   691  		}
   692  	}
   693  	// Remove existing contents, so that when we truncate to the actual size it will zero-fill,
   694  	// and we will be able to detect (some) incomplete writes as files containing trailing NUL bytes.
   695  	if err := f.Truncate(0); err != nil {
   696  		return err
   697  	}
   698  	// Reserve the final size and zero-fill.
   699  	if err := f.Truncate(int64(buf.Len())); err != nil {
   700  		return err
   701  	}
   702  	// Write the actual contents. If this fails partway through,
   703  	// the remainder of the file should remain as zeroes.
   704  	if _, err := f.Write(buf.Bytes()); err != nil {
   705  		f.Truncate(0)
   706  		return err
   707  	}
   708  
   709  	return nil
   710  }
   711  
   712  var (
   713  	statCacheOnce sync.Once
   714  	statCacheErr  error
   715  )
   716  
   717  // checkCacheDir checks if the directory specified by GOMODCACHE exists. An
   718  // error is returned if it does not.
   719  func checkCacheDir() error {
   720  	if cfg.GOMODCACHE == "" {
   721  		// modload.Init exits if GOPATH[0] is empty, and cfg.GOMODCACHE
   722  		// is set to GOPATH[0]/pkg/mod if GOMODCACHE is empty, so this should never happen.
   723  		return fmt.Errorf("internal error: cfg.GOMODCACHE not set")
   724  	}
   725  	if !filepath.IsAbs(cfg.GOMODCACHE) {
   726  		return fmt.Errorf("GOMODCACHE entry is relative; must be absolute path: %q.\n", cfg.GOMODCACHE)
   727  	}
   728  
   729  	// os.Stat is slow on Windows, so we only call it once to prevent unnecessary
   730  	// I/O every time this function is called.
   731  	statCacheOnce.Do(func() {
   732  		fi, err := os.Stat(cfg.GOMODCACHE)
   733  		if err != nil {
   734  			if !os.IsNotExist(err) {
   735  				statCacheErr = fmt.Errorf("could not create module cache: %w", err)
   736  				return
   737  			}
   738  			if err := os.MkdirAll(cfg.GOMODCACHE, 0777); err != nil {
   739  				statCacheErr = fmt.Errorf("could not create module cache: %w", err)
   740  				return
   741  			}
   742  			return
   743  		}
   744  		if !fi.IsDir() {
   745  			statCacheErr = fmt.Errorf("could not create module cache: %q is not a directory", cfg.GOMODCACHE)
   746  			return
   747  		}
   748  	})
   749  	return statCacheErr
   750  }
   751  

View as plain text