blob: b2cb44100ec4d28e24f711d06b7578287fcba06f [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 (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"cmd/go/internal/base"
"cmd/go/internal/gover"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"golang.org/x/mod/semver"
)
var (
vendorOnce sync.Once
vendorList []module.Version // modules that contribute packages to the build, in order of appearance
vendorReplaced []module.Version // all replaced modules; may or may not also contribute packages
vendorVersion map[string]string // module path → selected version (if known)
vendorPkgModule map[string]module.Version // package → containing module
vendorMeta map[module.Version]vendorMetadata
)
type vendorMetadata struct {
Explicit bool
Replacement module.Version
GoVersion string
}
// readVendorList reads the list of vendored modules from vendor/modules.txt.
func readVendorList(vendorDir string) {
vendorOnce.Do(func() {
vendorList = nil
vendorPkgModule = make(map[string]module.Version)
vendorVersion = make(map[string]string)
vendorMeta = make(map[module.Version]vendorMetadata)
vendorFile := filepath.Join(vendorDir, "modules.txt")
data, err := os.ReadFile(vendorFile)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
base.Fatalf("go: %s", err)
}
return
}
var mod module.Version
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, "# ") {
f := strings.Fields(line)
if len(f) < 3 {
continue
}
if semver.IsValid(f[2]) {
// A module, but we don't yet know whether it is in the build list or
// only included to indicate a replacement.
mod = module.Version{Path: f[1], Version: f[2]}
f = f[3:]
} else if f[2] == "=>" {
// A wildcard replacement found in the main module's go.mod file.
mod = module.Version{Path: f[1]}
f = f[2:]
} else {
// Not a version or a wildcard replacement.
// We don't know how to interpret this module line, so ignore it.
mod = module.Version{}
continue
}
if len(f) >= 2 && f[0] == "=>" {
meta := vendorMeta[mod]
if len(f) == 2 {
// File replacement.
meta.Replacement = module.Version{Path: f[1]}
vendorReplaced = append(vendorReplaced, mod)
} else if len(f) == 3 && semver.IsValid(f[2]) {
// Path and version replacement.
meta.Replacement = module.Version{Path: f[1], Version: f[2]}
vendorReplaced = append(vendorReplaced, mod)
} else {
// We don't understand this replacement. Ignore it.
}
vendorMeta[mod] = meta
}
continue
}
// Not a module line. Must be a package within a module or a metadata
// directive, either of which requires a preceding module line.
if mod.Path == "" {
continue
}
if annotations, ok := strings.CutPrefix(line, "## "); ok {
// Metadata. Take the union of annotations across multiple lines, if present.
meta := vendorMeta[mod]
for _, entry := range strings.Split(annotations, ";") {
entry = strings.TrimSpace(entry)
if entry == "explicit" {
meta.Explicit = true
}
if goVersion, ok := strings.CutPrefix(entry, "go "); ok {
meta.GoVersion = goVersion
rawGoVersion.Store(mod, meta.GoVersion)
if gover.Compare(goVersion, gover.Local()) > 0 {
base.Fatal(&gover.TooNewError{What: mod.Path + " in " + base.ShortPath(vendorFile), GoVersion: goVersion})
}
}
// All other tokens are reserved for future use.
}
vendorMeta[mod] = meta
continue
}
if f := strings.Fields(line); len(f) == 1 && module.CheckImportPath(f[0]) == nil {
// A package within the current module.
vendorPkgModule[f[0]] = mod
// Since this module provides a package for the build, we know that it
// is in the build list and is the selected version of its path.
// If this information is new, record it.
if v, ok := vendorVersion[mod.Path]; !ok || gover.ModCompare(mod.Path, v, mod.Version) < 0 {
vendorList = append(vendorList, mod)
vendorVersion[mod.Path] = mod.Version
}
}
}
})
}
// checkVendorConsistency verifies that the vendor/modules.txt file matches (if
// go 1.14) or at least does not contradict (go 1.13 or earlier) the
// requirements and replacements listed in the main module's go.mod file.
func checkVendorConsistency(indexes []*modFileIndex, modFiles []*modfile.File, modRoots []string) {
// readVendorList only needs the main module to get the directory
// the vendor directory is in.
readVendorList(VendorDir())
if len(modFiles) < 1 {
// We should never get here if there are zero modfiles. Either
// we're in single module mode and there's a single module, or
// we're in workspace mode, and we fail earlier reporting that
// "no modules were found in the current workspace".
panic("checkVendorConsistency called with zero modfiles")
}
pre114 := false
if !inWorkspaceMode() { // workspace mode was added after Go 1.14
if len(indexes) != 1 {
panic(fmt.Errorf("not in workspace mode but number of indexes is %v, not 1", len(indexes)))
}
index := indexes[0]
if gover.Compare(index.goVersion, "1.14") < 0 {
// Go versions before 1.14 did not include enough information in
// vendor/modules.txt to check for consistency.
// If we know that we're on an earlier version, relax the consistency check.
pre114 = true
}
}
vendErrors := new(strings.Builder)
vendErrorf := func(mod module.Version, format string, args ...any) {
detail := fmt.Sprintf(format, args...)
if mod.Version == "" {
fmt.Fprintf(vendErrors, "\n\t%s: %s", mod.Path, detail)
} else {
fmt.Fprintf(vendErrors, "\n\t%s@%s: %s", mod.Path, mod.Version, detail)
}
}
// Iterate over the Require directives in their original (not indexed) order
// so that the errors match the original file.
for _, modFile := range modFiles {
for _, r := range modFile.Require {
if !vendorMeta[r.Mod].Explicit {
if pre114 {
// Before 1.14, modules.txt did not indicate whether modules were listed
// explicitly in the main module's go.mod file.
// However, we can at least detect a version mismatch if packages were
// vendored from a non-matching version.
if vv, ok := vendorVersion[r.Mod.Path]; ok && vv != r.Mod.Version {
vendErrorf(r.Mod, fmt.Sprintf("is explicitly required in go.mod, but vendor/modules.txt indicates %s@%s", r.Mod.Path, vv))
}
} else {
vendErrorf(r.Mod, "is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt")
}
}
}
}
describe := func(m module.Version) string {
if m.Version == "" {
return m.Path
}
return m.Path + "@" + m.Version
}
// We need to verify *all* replacements that occur in modfile: even if they
// don't directly apply to any module in the vendor list, the replacement
// go.mod file can affect the selected versions of other (transitive)
// dependencies
seenrep := make(map[module.Version]bool)
checkReplace := func(replaces []*modfile.Replace) {
for _, r := range replaces {
if seenrep[r.Old] {
continue // Don't print the same error more than once
}
seenrep[r.Old] = true
rNew, modRoot, replacementSource := replacementFrom(r.Old)
rNewCanonical := canonicalizeReplacePath(rNew, modRoot)
vr := vendorMeta[r.Old].Replacement
if vr == (module.Version{}) {
if rNewCanonical == (module.Version{}) {
// r.Old is not actually replaced. It might be a main module.
// Don't return an error.
} else if pre114 && (r.Old.Version == "" || vendorVersion[r.Old.Path] != r.Old.Version) {
// Before 1.14, modules.txt omitted wildcard replacements and
// replacements for modules that did not have any packages to vendor.
} else {
vendErrorf(r.Old, "is replaced in %s, but not marked as replaced in vendor/modules.txt", base.ShortPath(replacementSource))
}
} else if vr != rNewCanonical {
vendErrorf(r.Old, "is replaced by %s in %s, but marked as replaced by %s in vendor/modules.txt", describe(rNew), base.ShortPath(replacementSource), describe(vr))
}
}
}
for _, modFile := range modFiles {
checkReplace(modFile.Replace)
}
if MainModules.workFile != nil {
checkReplace(MainModules.workFile.Replace)
}
for _, mod := range vendorList {
meta := vendorMeta[mod]
if meta.Explicit {
// in workspace mode, check that it's required by at least one of the main modules
var foundRequire bool
for _, index := range indexes {
if _, inGoMod := index.require[mod]; inGoMod {
foundRequire = true
}
}
if !foundRequire {
article := ""
if inWorkspaceMode() {
article = "a "
}
vendErrorf(mod, "is marked as explicit in vendor/modules.txt, but not explicitly required in %vgo.mod", article)
}
}
}
for _, mod := range vendorReplaced {
r := Replacement(mod)
replacementSource := "go.mod"
if inWorkspaceMode() {
replacementSource = "the workspace"
}
if r == (module.Version{}) {
vendErrorf(mod, "is marked as replaced in vendor/modules.txt, but not replaced in %s", replacementSource)
continue
}
// If both replacements exist, we've already reported that they're different above.
}
if vendErrors.Len() > 0 {
subcmd := "mod"
if inWorkspaceMode() {
subcmd = "work"
}
base.Fatalf("go: inconsistent vendoring in %s:%s\n\n\tTo ignore the vendor directory, use -mod=readonly or -mod=mod.\n\tTo sync the vendor directory, run:\n\t\tgo %s vendor", filepath.Dir(VendorDir()), vendErrors, subcmd)
}
}