1// Copyright 2020 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
5package modload
6
7import (
8	"errors"
9	"fmt"
10	"io/fs"
11	"os"
12	"path/filepath"
13	"strings"
14	"sync"
15
16	"cmd/go/internal/base"
17	"cmd/go/internal/gover"
18
19	"golang.org/x/mod/modfile"
20	"golang.org/x/mod/module"
21	"golang.org/x/mod/semver"
22)
23
24var (
25	vendorOnce      sync.Once
26	vendorList      []module.Version          // modules that contribute packages to the build, in order of appearance
27	vendorReplaced  []module.Version          // all replaced modules; may or may not also contribute packages
28	vendorVersion   map[string]string         // module path → selected version (if known)
29	vendorPkgModule map[string]module.Version // package → containing module
30	vendorMeta      map[module.Version]vendorMetadata
31)
32
33type vendorMetadata struct {
34	Explicit    bool
35	Replacement module.Version
36	GoVersion   string
37}
38
39// readVendorList reads the list of vendored modules from vendor/modules.txt.
40func readVendorList(vendorDir string) {
41	vendorOnce.Do(func() {
42		vendorList = nil
43		vendorPkgModule = make(map[string]module.Version)
44		vendorVersion = make(map[string]string)
45		vendorMeta = make(map[module.Version]vendorMetadata)
46		vendorFile := filepath.Join(vendorDir, "modules.txt")
47		data, err := os.ReadFile(vendorFile)
48		if err != nil {
49			if !errors.Is(err, fs.ErrNotExist) {
50				base.Fatalf("go: %s", err)
51			}
52			return
53		}
54
55		var mod module.Version
56		for _, line := range strings.Split(string(data), "\n") {
57			if strings.HasPrefix(line, "# ") {
58				f := strings.Fields(line)
59
60				if len(f) < 3 {
61					continue
62				}
63				if semver.IsValid(f[2]) {
64					// A module, but we don't yet know whether it is in the build list or
65					// only included to indicate a replacement.
66					mod = module.Version{Path: f[1], Version: f[2]}
67					f = f[3:]
68				} else if f[2] == "=>" {
69					// A wildcard replacement found in the main module's go.mod file.
70					mod = module.Version{Path: f[1]}
71					f = f[2:]
72				} else {
73					// Not a version or a wildcard replacement.
74					// We don't know how to interpret this module line, so ignore it.
75					mod = module.Version{}
76					continue
77				}
78
79				if len(f) >= 2 && f[0] == "=>" {
80					meta := vendorMeta[mod]
81					if len(f) == 2 {
82						// File replacement.
83						meta.Replacement = module.Version{Path: f[1]}
84						vendorReplaced = append(vendorReplaced, mod)
85					} else if len(f) == 3 && semver.IsValid(f[2]) {
86						// Path and version replacement.
87						meta.Replacement = module.Version{Path: f[1], Version: f[2]}
88						vendorReplaced = append(vendorReplaced, mod)
89					} else {
90						// We don't understand this replacement. Ignore it.
91					}
92					vendorMeta[mod] = meta
93				}
94				continue
95			}
96
97			// Not a module line. Must be a package within a module or a metadata
98			// directive, either of which requires a preceding module line.
99			if mod.Path == "" {
100				continue
101			}
102
103			if annotations, ok := strings.CutPrefix(line, "## "); ok {
104				// Metadata. Take the union of annotations across multiple lines, if present.
105				meta := vendorMeta[mod]
106				for _, entry := range strings.Split(annotations, ";") {
107					entry = strings.TrimSpace(entry)
108					if entry == "explicit" {
109						meta.Explicit = true
110					}
111					if goVersion, ok := strings.CutPrefix(entry, "go "); ok {
112						meta.GoVersion = goVersion
113						rawGoVersion.Store(mod, meta.GoVersion)
114						if gover.Compare(goVersion, gover.Local()) > 0 {
115							base.Fatal(&gover.TooNewError{What: mod.Path + " in " + base.ShortPath(vendorFile), GoVersion: goVersion})
116						}
117					}
118					// All other tokens are reserved for future use.
119				}
120				vendorMeta[mod] = meta
121				continue
122			}
123
124			if f := strings.Fields(line); len(f) == 1 && module.CheckImportPath(f[0]) == nil {
125				// A package within the current module.
126				vendorPkgModule[f[0]] = mod
127
128				// Since this module provides a package for the build, we know that it
129				// is in the build list and is the selected version of its path.
130				// If this information is new, record it.
131				if v, ok := vendorVersion[mod.Path]; !ok || gover.ModCompare(mod.Path, v, mod.Version) < 0 {
132					vendorList = append(vendorList, mod)
133					vendorVersion[mod.Path] = mod.Version
134				}
135			}
136		}
137	})
138}
139
140// checkVendorConsistency verifies that the vendor/modules.txt file matches (if
141// go 1.14) or at least does not contradict (go 1.13 or earlier) the
142// requirements and replacements listed in the main module's go.mod file.
143func checkVendorConsistency(indexes []*modFileIndex, modFiles []*modfile.File, modRoots []string) {
144	// readVendorList only needs the main module to get the directory
145	// the vendor directory is in.
146	readVendorList(VendorDir())
147
148	if len(modFiles) < 1 {
149		// We should never get here if there are zero modfiles. Either
150		// we're in single module mode and there's a single module, or
151		// we're in workspace mode, and we fail earlier reporting that
152		// "no modules were found in the current workspace".
153		panic("checkVendorConsistency called with zero modfiles")
154	}
155
156	pre114 := false
157	if !inWorkspaceMode() { // workspace mode was added after Go 1.14
158		if len(indexes) != 1 {
159			panic(fmt.Errorf("not in workspace mode but number of indexes is %v, not 1", len(indexes)))
160		}
161		index := indexes[0]
162		if gover.Compare(index.goVersion, "1.14") < 0 {
163			// Go versions before 1.14 did not include enough information in
164			// vendor/modules.txt to check for consistency.
165			// If we know that we're on an earlier version, relax the consistency check.
166			pre114 = true
167		}
168	}
169
170	vendErrors := new(strings.Builder)
171	vendErrorf := func(mod module.Version, format string, args ...any) {
172		detail := fmt.Sprintf(format, args...)
173		if mod.Version == "" {
174			fmt.Fprintf(vendErrors, "\n\t%s: %s", mod.Path, detail)
175		} else {
176			fmt.Fprintf(vendErrors, "\n\t%s@%s: %s", mod.Path, mod.Version, detail)
177		}
178	}
179
180	// Iterate over the Require directives in their original (not indexed) order
181	// so that the errors match the original file.
182	for _, modFile := range modFiles {
183		for _, r := range modFile.Require {
184			if !vendorMeta[r.Mod].Explicit {
185				if pre114 {
186					// Before 1.14, modules.txt did not indicate whether modules were listed
187					// explicitly in the main module's go.mod file.
188					// However, we can at least detect a version mismatch if packages were
189					// vendored from a non-matching version.
190					if vv, ok := vendorVersion[r.Mod.Path]; ok && vv != r.Mod.Version {
191						vendErrorf(r.Mod, fmt.Sprintf("is explicitly required in go.mod, but vendor/modules.txt indicates %s@%s", r.Mod.Path, vv))
192					}
193				} else {
194					vendErrorf(r.Mod, "is explicitly required in go.mod, but not marked as explicit in vendor/modules.txt")
195				}
196			}
197		}
198	}
199
200	describe := func(m module.Version) string {
201		if m.Version == "" {
202			return m.Path
203		}
204		return m.Path + "@" + m.Version
205	}
206
207	// We need to verify *all* replacements that occur in modfile: even if they
208	// don't directly apply to any module in the vendor list, the replacement
209	// go.mod file can affect the selected versions of other (transitive)
210	// dependencies
211	seenrep := make(map[module.Version]bool)
212	checkReplace := func(replaces []*modfile.Replace) {
213		for _, r := range replaces {
214			if seenrep[r.Old] {
215				continue // Don't print the same error more than once
216			}
217			seenrep[r.Old] = true
218			rNew, modRoot, replacementSource := replacementFrom(r.Old)
219			rNewCanonical := canonicalizeReplacePath(rNew, modRoot)
220			vr := vendorMeta[r.Old].Replacement
221			if vr == (module.Version{}) {
222				if rNewCanonical == (module.Version{}) {
223					// r.Old is not actually replaced. It might be a main module.
224					// Don't return an error.
225				} else if pre114 && (r.Old.Version == "" || vendorVersion[r.Old.Path] != r.Old.Version) {
226					// Before 1.14, modules.txt omitted wildcard replacements and
227					// replacements for modules that did not have any packages to vendor.
228				} else {
229					vendErrorf(r.Old, "is replaced in %s, but not marked as replaced in vendor/modules.txt", base.ShortPath(replacementSource))
230				}
231			} else if vr != rNewCanonical {
232				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))
233			}
234		}
235	}
236	for _, modFile := range modFiles {
237		checkReplace(modFile.Replace)
238	}
239	if MainModules.workFile != nil {
240		checkReplace(MainModules.workFile.Replace)
241	}
242
243	for _, mod := range vendorList {
244		meta := vendorMeta[mod]
245		if meta.Explicit {
246			// in workspace mode, check that it's required by at least one of the main modules
247			var foundRequire bool
248			for _, index := range indexes {
249				if _, inGoMod := index.require[mod]; inGoMod {
250					foundRequire = true
251				}
252			}
253			if !foundRequire {
254				article := ""
255				if inWorkspaceMode() {
256					article = "a "
257				}
258				vendErrorf(mod, "is marked as explicit in vendor/modules.txt, but not explicitly required in %vgo.mod", article)
259			}
260
261		}
262	}
263
264	for _, mod := range vendorReplaced {
265		r := Replacement(mod)
266		replacementSource := "go.mod"
267		if inWorkspaceMode() {
268			replacementSource = "the workspace"
269		}
270		if r == (module.Version{}) {
271			vendErrorf(mod, "is marked as replaced in vendor/modules.txt, but not replaced in %s", replacementSource)
272			continue
273		}
274		// If both replacements exist, we've already reported that they're different above.
275	}
276
277	if vendErrors.Len() > 0 {
278		subcmd := "mod"
279		if inWorkspaceMode() {
280			subcmd = "work"
281		}
282		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)
283	}
284}
285