// 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) } }