...

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

View as plain text