blob: c04e2add1389ce8f2699478ebc265717a6164949 [file] [log] [blame]
// Copyright 2020 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 modload
import (
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/lockedfile"
"cmd/go/internal/modfetch"
"cmd/go/internal/par"
"errors"
"fmt"
"path/filepath"
"sync"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
var modFile *modfile.File
// A modFileIndex is an index of data corresponding to a modFile
// at a specific point in time.
type modFileIndex struct {
data []byte
dataNeedsFix bool // true if fixVersion applied a change while parsing data
module module.Version
goVersionV string // GoVersion with "v" prefix
require map[module.Version]requireMeta
replace map[module.Version]module.Version
exclude map[module.Version]bool
}
// index is the index of the go.mod file as of when it was last read or written.
var index *modFileIndex
type requireMeta struct {
indirect bool
}
// Allowed reports whether module m is allowed (not excluded) by the main module's go.mod.
func Allowed(m module.Version) bool {
return index == nil || !index.exclude[m]
}
// Replacement returns the replacement for mod, if any, from go.mod.
// If there is no replacement for mod, Replacement returns
// a module.Version with Path == "".
func Replacement(mod module.Version) module.Version {
if index != nil {
if r, ok := index.replace[mod]; ok {
return r
}
if r, ok := index.replace[module.Version{Path: mod.Path}]; ok {
return r
}
}
return module.Version{}
}
// indexModFile rebuilds the index of modFile.
// If modFile has been changed since it was first read,
// modFile.Cleanup must be called before indexModFile.
func indexModFile(data []byte, modFile *modfile.File, needsFix bool) *modFileIndex {
i := new(modFileIndex)
i.data = data
i.dataNeedsFix = needsFix
i.module = module.Version{}
if modFile.Module != nil {
i.module = modFile.Module.Mod
}
i.goVersionV = ""
if modFile.Go != nil {
// We're going to use the semver package to compare Go versions, so go ahead
// and add the "v" prefix it expects once instead of every time.
i.goVersionV = "v" + modFile.Go.Version
}
i.require = make(map[module.Version]requireMeta, len(modFile.Require))
for _, r := range modFile.Require {
i.require[r.Mod] = requireMeta{indirect: r.Indirect}
}
i.replace = make(map[module.Version]module.Version, len(modFile.Replace))
for _, r := range modFile.Replace {
if prev, dup := i.replace[r.Old]; dup && prev != r.New {
base.Fatalf("go: conflicting replacements for %v:\n\t%v\n\t%v", r.Old, prev, r.New)
}
i.replace[r.Old] = r.New
}
i.exclude = make(map[module.Version]bool, len(modFile.Exclude))
for _, x := range modFile.Exclude {
i.exclude[x.Mod] = true
}
return i
}
// modFileIsDirty reports whether the go.mod file differs meaningfully
// from what was indexed.
// If modFile has been changed (even cosmetically) since it was first read,
// modFile.Cleanup must be called before modFileIsDirty.
func (i *modFileIndex) modFileIsDirty(modFile *modfile.File) bool {
if i == nil {
return modFile != nil
}
if i.dataNeedsFix {
return true
}
if modFile.Module == nil {
if i.module != (module.Version{}) {
return true
}
} else if modFile.Module.Mod != i.module {
return true
}
if modFile.Go == nil {
if i.goVersionV != "" {
return true
}
} else if "v"+modFile.Go.Version != i.goVersionV {
if i.goVersionV == "" && cfg.BuildMod == "readonly" {
// go.mod files did not always require a 'go' version, so do not error out
// if one is missing — we may be inside an older module in the module
// cache, and should bias toward providing useful behavior.
} else {
return true
}
}
if len(modFile.Require) != len(i.require) ||
len(modFile.Replace) != len(i.replace) ||
len(modFile.Exclude) != len(i.exclude) {
return true
}
for _, r := range modFile.Require {
if meta, ok := i.require[r.Mod]; !ok {
return true
} else if r.Indirect != meta.indirect {
if cfg.BuildMod == "readonly" {
// The module's requirements are consistent; only the "// indirect"
// comments that are wrong. But those are only guaranteed to be accurate
// after a "go mod tidy" — it's a good idea to run those before
// committing a change, but it's certainly not mandatory.
} else {
return true
}
}
}
for _, r := range modFile.Replace {
if r.New != i.replace[r.Old] {
return true
}
}
for _, x := range modFile.Exclude {
if !i.exclude[x.Mod] {
return true
}
}
return false
}
// rawGoVersion records the Go version parsed from each module's go.mod file.
//
// If a module is replaced, the version of the replacement is keyed by the
// replacement module.Version, not the version being replaced.
var rawGoVersion sync.Map // map[module.Version]string
// A modFileSummary is a summary of a go.mod file for which we do not need to
// retain complete information — for example, the go.mod file of a dependency
// module.
type modFileSummary struct {
module module.Version
goVersionV string // GoVersion with "v" prefix
require []module.Version
}
// goModSummary returns a summary of the go.mod file for module m,
// taking into account any replacements for m, exclusions of its dependencies,
// and or vendoring.
//
// goModSummary cannot be used on the Target module, as its requirements
// may change.
//
// The caller must not modify the returned summary.
func goModSummary(m module.Version) (*modFileSummary, error) {
if m == Target {
panic("internal error: goModSummary called on the Target module")
}
type cached struct {
summary *modFileSummary
err error
}
c := goModSummaryCache.Do(m, func() interface{} {
if cfg.BuildMod == "vendor" {
summary := &modFileSummary{
module: module.Version{Path: m.Path},
}
if vendorVersion[m.Path] != m.Version {
// This module is not vendored, so packages cannot be loaded from it and
// it cannot be relevant to the build.
return cached{summary, nil}
}
// For every module other than the target,
// return the full list of modules from modules.txt.
readVendorList()
// TODO(#36876): Load the "go" version from vendor/modules.txt and store it
// in rawGoVersion with the appropriate key.
// We don't know what versions the vendored module actually relies on,
// so assume that it requires everything.
summary.require = vendorList
return cached{summary, nil}
}
actual := Replacement(m)
if actual.Path == "" {
actual = m
}
summary, err := rawGoModSummary(actual)
if err != nil {
return cached{nil, err}
}
if actual.Version == "" {
// The actual module is a filesystem-local replacement, for which we have
// unfortunately not enforced any sort of invariants about module lines or
// matching module paths. Anything goes.
//
// TODO(bcmills): Remove this special-case, update tests, and add a
// release note.
} else {
if summary.module.Path == "" {
return cached{nil, module.VersionError(actual, errors.New("parsing go.mod: missing module line"))}
}
// In theory we should only allow mpath to be unequal to mod.Path here if the
// version that we fetched lacks an explicit go.mod file: if the go.mod file
// is explicit, then it should match exactly (to ensure that imports of other
// packages within the module are interpreted correctly). Unfortunately, we
// can't determine that information from the module proxy protocol: we'll have
// to leave that validation for when we load actual packages from within the
// module.
if mpath := summary.module.Path; mpath != m.Path && mpath != actual.Path {
return cached{nil, module.VersionError(actual, fmt.Errorf(`parsing go.mod:
module declares its path as: %s
but was required as: %s`, mpath, m.Path))}
}
}
if index != nil && len(index.exclude) > 0 {
// Drop any requirements on excluded versions.
nonExcluded := summary.require[:0]
for _, r := range summary.require {
if !index.exclude[r] {
nonExcluded = append(nonExcluded, r)
}
}
summary.require = nonExcluded
}
return cached{summary, nil}
}).(cached)
return c.summary, c.err
}
var goModSummaryCache par.Cache // module.Version → goModSummary result
// rawGoModSummary returns a new summary of the go.mod file for module m,
// ignoring all replacements that may apply to m and excludes that may apply to
// its dependencies.
//
// rawGoModSummary cannot be used on the Target module.
func rawGoModSummary(m module.Version) (*modFileSummary, error) {
if m == Target {
panic("internal error: rawGoModSummary called on the Target module")
}
summary := new(modFileSummary)
var f *modfile.File
if m.Version == "" {
// m is a replacement module with only a file path.
dir := m.Path
if !filepath.IsAbs(dir) {
dir = filepath.Join(ModRoot(), dir)
}
gomod := filepath.Join(dir, "go.mod")
data, err := lockedfile.Read(gomod)
if err != nil {
return nil, module.VersionError(m, fmt.Errorf("reading %s: %v", base.ShortPath(gomod), err))
}
f, err = modfile.ParseLax(gomod, data, nil)
if err != nil {
return nil, module.VersionError(m, fmt.Errorf("parsing %s: %v", base.ShortPath(gomod), err))
}
} else {
if !semver.IsValid(m.Version) {
// Disallow the broader queries supported by fetch.Lookup.
base.Fatalf("go: internal error: %s@%s: unexpected invalid semantic version", m.Path, m.Version)
}
data, err := modfetch.GoMod(m.Path, m.Version)
if err != nil {
return nil, err
}
f, err = modfile.ParseLax("go.mod", data, nil)
if err != nil {
return nil, module.VersionError(m, fmt.Errorf("parsing go.mod: %v", err))
}
}
if f.Module != nil {
summary.module = f.Module.Mod
}
if f.Go != nil && f.Go.Version != "" {
rawGoVersion.LoadOrStore(m, f.Go.Version)
summary.goVersionV = "v" + f.Go.Version
}
if len(f.Require) > 0 {
summary.require = make([]module.Version, 0, len(f.Require))
for _, req := range f.Require {
summary.require = append(summary.require, req.Mod)
}
}
return summary, nil
}