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

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

View as plain text