blob: cc785ec332165cb754136050ee457b73431bb2f6 [file] [log] [blame]
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package modfetch
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/par"
"cmd/go/internal/semver"
)
var QuietLookup bool // do not print about lookups
var SrcMod string // $GOPATH/src/mod; set by package vgo
// A cachingRepo is a cache around an underlying Repo,
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
// It is also safe for simultaneous use by multiple goroutines
// (so that it can be returned from Lookup multiple times).
// It serializes calls to the underlying Repo.
type cachingRepo struct {
path string
cache par.Cache // cache for all operations
r Repo
}
func newCachingRepo(r Repo) *cachingRepo {
return &cachingRepo{
r: r,
path: r.ModulePath(),
}
}
func (r *cachingRepo) ModulePath() string {
return r.path
}
func (r *cachingRepo) Versions(prefix string) ([]string, error) {
type cached struct {
list []string
err error
}
c := r.cache.Do("versions:"+prefix, func() interface{} {
list, err := r.r.Versions(prefix)
return cached{list, err}
}).(cached)
if c.err != nil {
return nil, c.err
}
return append([]string(nil), c.list...), nil
}
type cachedInfo struct {
info *RevInfo
err error
}
func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
c := r.cache.Do("stat:"+rev, func() interface{} {
file, info, err := readDiskStat(r.path, rev)
if err == nil {
return cachedInfo{info, nil}
}
if !QuietLookup {
fmt.Fprintf(os.Stderr, "vgo: finding %s %s\n", r.path, rev)
}
info, err = r.r.Stat(rev)
if err == nil {
if err := writeDiskStat(file, info); err != nil {
fmt.Fprintf(os.Stderr, "go: writing stat cache: %v\n", err)
}
// If we resolved, say, 1234abcde to v0.0.0-20180604122334-1234abcdef78,
// then save the information under the proper version, for future use.
if info.Version != rev {
r.cache.Do("stat:"+info.Version, func() interface{} {
return cachedInfo{info, err}
})
}
}
return cachedInfo{info, err}
}).(cachedInfo)
if c.err != nil {
return nil, c.err
}
info := *c.info
return &info, nil
}
func (r *cachingRepo) Latest() (*RevInfo, error) {
c := r.cache.Do("latest:", func() interface{} {
if !QuietLookup {
fmt.Fprintf(os.Stderr, "vgo: finding %s latest\n", r.path)
}
info, err := r.r.Latest()
// Save info for likely future Stat call.
if err == nil {
r.cache.Do("stat:"+info.Version, func() interface{} {
return cachedInfo{info, err}
})
if file, _, err := readDiskStat(r.path, info.Version); err != nil {
writeDiskStat(file, info)
}
}
return cachedInfo{info, err}
}).(cachedInfo)
if c.err != nil {
return nil, c.err
}
info := *c.info
return &info, nil
}
func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
type cached struct {
text []byte
err error
}
c := r.cache.Do("gomod:"+rev, func() interface{} {
file, text, err := readDiskGoMod(r.path, rev)
if err == nil {
// Note: readDiskGoMod already called checkGoMod.
return cached{text, nil}
}
// Convert rev to canonical version
// so that we use the right identifier in the go.sum check.
info, err := r.Stat(rev)
if err != nil {
return cached{nil, err}
}
rev = info.Version
text, err = r.r.GoMod(rev)
checkGoMod(r.path, rev, text)
if err == nil {
if err := writeDiskGoMod(file, text); err != nil {
fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
}
}
return cached{text, err}
}).(cached)
if c.err != nil {
return nil, c.err
}
return append([]byte(nil), c.text...), nil
}
func (r *cachingRepo) Zip(version, tmpdir string) (string, error) {
return r.r.Zip(version, tmpdir)
}
// Stat is like Lookup(path).Stat(rev) but avoids the
// repository path resolution in Lookup if the result is
// already cached on local disk.
func Stat(path, rev string) (*RevInfo, error) {
_, info, err := readDiskStat(path, rev)
if err == nil {
return info, nil
}
repo, err := Lookup(path)
if err != nil {
return nil, err
}
return repo.Stat(rev)
}
// GoMod is like Lookup(path).GoMod(rev) but avoids the
// repository path resolution in Lookup if the result is
// already cached on local disk.
func GoMod(path, rev string) ([]byte, error) {
// Convert commit hash to pseudo-version
// to increase cache hit rate.
if !semver.IsValid(rev) {
info, err := Stat(path, rev)
if err != nil {
return nil, err
}
rev = info.Version
}
_, data, err := readDiskGoMod(path, rev)
if err == nil {
return data, nil
}
repo, err := Lookup(path)
if err != nil {
return nil, err
}
return repo.GoMod(rev)
}
var errNotCached = fmt.Errorf("not in cache")
// readDiskStat reads a cached stat result from disk,
// returning the name of the cache file and the result.
// If the read fails, the caller can use
// writeDiskStat(file, info) to write a new cache entry.
func readDiskStat(path, rev string) (file string, info *RevInfo, err error) {
file, data, err := readDiskCache(path, rev, "info")
if err != nil {
if file, info, err := readDiskStatByHash(path, rev); err == nil {
return file, info, nil
}
return file, nil, err
}
info = new(RevInfo)
if err := json.Unmarshal(data, info); err != nil {
return file, nil, errNotCached
}
return file, info, nil
}
// readDiskStatByHash is a fallback for readDiskStat for the case
// where rev is a commit hash instead of a proper semantic version.
// In that case, we look for a cached pseudo-version that matches
// the commit hash. If we find one, we use it.
// This matters most for converting legacy package management
// configs, when we are often looking up commits by full hash.
// Without this check we'd be doing network I/O to the remote repo
// just to find out about a commit we already know about
// (and have cached under its pseudo-version).
func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error) {
if !codehost.AllHex(rev) || len(rev) < 12 {
return "", nil, errNotCached
}
rev = rev[:12]
dir, err := os.Open(filepath.Join(SrcMod, "cache/download", path, "@v"))
if err != nil {
return "", nil, errNotCached
}
names, err := dir.Readdirnames(-1)
dir.Close()
if err != nil {
return "", nil, errNotCached
}
suffix := "-" + rev + ".info"
for _, name := range names {
if strings.HasSuffix(name, suffix) && IsPseudoVersion(strings.TrimSuffix(name, ".info")) {
return readDiskStat(path, strings.TrimSuffix(name, ".info"))
}
}
return "", nil, errNotCached
}
// oldVgoPrefix is the prefix in the old auto-generated cached go.mod files.
// We stopped trying to auto-generate the go.mod files. Now we use a trivial
// go.mod with only a module line, and we've dropped the version prefix
// entirely. If we see a version prefix, that means we're looking at an old copy
// and should ignore it.
var oldVgoPrefix = []byte("//vgo 0.0.")
// readDiskGoMod reads a cached stat result from disk,
// returning the name of the cache file and the result.
// If the read fails, the caller can use
// writeDiskGoMod(file, data) to write a new cache entry.
func readDiskGoMod(path, rev string) (file string, data []byte, err error) {
file, data, err = readDiskCache(path, rev, "mod")
// If the file has an old auto-conversion prefix, pretend it's not there.
if bytes.HasPrefix(data, oldVgoPrefix) {
err = errNotCached
data = nil
}
if err == nil {
checkGoMod(path, rev, data)
}
return file, data, err
}
// readDiskCache is the generic "read from a cache file" implementation.
// It takes the revision and an identifying suffix for the kind of data being cached.
// It returns the name of the cache file and the content of the file.
// If the read fails, the caller can use
// writeDiskCache(file, data) to write a new cache entry.
func readDiskCache(path, rev, suffix string) (file string, data []byte, err error) {
if !semver.IsValid(rev) || SrcMod == "" {
return "", nil, errNotCached
}
file = filepath.Join(SrcMod, "cache/download", path, "@v", rev+"."+suffix)
data, err = ioutil.ReadFile(file)
if err != nil {
return file, nil, errNotCached
}
return file, data, nil
}
// writeDiskStat writes a stat result cache entry.
// The file name must have been returned by a previous call to readDiskStat.
func writeDiskStat(file string, info *RevInfo) error {
if file == "" {
return nil
}
js, err := json.Marshal(info)
if err != nil {
return err
}
return writeDiskCache(file, js)
}
// writeDiskGoMod writes a go.mod cache entry.
// The file name must have been returned by a previous call to readDiskGoMod.
func writeDiskGoMod(file string, text []byte) error {
return writeDiskCache(file, text)
}
// writeDiskCache is the generic "write to a cache file" implementation.
// The file must have been returned by a previous call to readDiskCache.
func writeDiskCache(file string, data []byte) error {
if file == "" {
return nil
}
// Make sure directory for file exists.
if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
return err
}
// Write data to temp file next to target file.
f, err := ioutil.TempFile(filepath.Dir(file), filepath.Base(file)+".tmp-")
if err != nil {
return err
}
defer os.Remove(f.Name())
defer f.Close()
if _, err := f.Write(data); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
// Rename temp file onto cache file,
// so that the cache file is always a complete file.
return os.Rename(f.Name(), file)
}