mustUseModules = false
initialized bool
- modRoot string
- modFile *modfile.File
- modFileData []byte
- excluded map[module.Version]bool
- Target module.Version
+ modRoot string
+ Target module.Version
// targetPrefix is the path prefix for packages in Target, without a trailing
// slash. For most modules, targetPrefix is just Target.Path, but the
allowMissingModuleImports bool
)
+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
+ goVersion string
+ 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
+}
+
// ModFile returns the parsed go.mod file.
//
// Note that after calling ImportPaths or LoadBuildList,
base.Fatalf("go: %v", err)
}
- f, err := modfile.Parse(gomod, data, fixVersion)
+ var fixed bool
+ f, err := modfile.Parse(gomod, data, fixVersion(&fixed))
if err != nil {
// Errors returned by modfile.Parse begin with file:line.
base.Fatalf("go: errors parsing go.mod:\n%s\n", err)
}
modFile = f
- modFileData = data
+ index = indexModFile(data, f, fixed)
if len(f.Syntax.Stmt) == 0 || f.Module == nil {
// Empty mod file. Must add module path.
legacyModInit()
}
- excluded = make(map[module.Version]bool)
- for _, x := range f.Exclude {
- excluded[x.Mod] = true
- }
modFileToBuildList()
setDefaultBuildMod()
if cfg.BuildMod == "vendor" {
}
}
+// fixVersion returns a modfile.VersionFixer implemented using the Query function.
+//
+// It resolves commit hashes and branch names to versions,
+// canonicalizes verisons that appeared in early vgo drafts,
+// and does nothing for versions that already appear to be canonical.
+//
+// The VersionFixer sets 'fixed' if it ever returns a non-canonical version.
+func fixVersion(fixed *bool) modfile.VersionFixer {
+ return func(path, vers string) (resolved string, err error) {
+ defer func() {
+ if err == nil && resolved != vers {
+ *fixed = true
+ }
+ }()
+
+ // Special case: remove the old -gopkgin- hack.
+ if strings.HasPrefix(path, "gopkg.in/") && strings.Contains(vers, "-gopkgin-") {
+ vers = vers[strings.Index(vers, "-gopkgin-")+len("-gopkgin-"):]
+ }
+
+ // fixVersion is called speculatively on every
+ // module, version pair from every go.mod file.
+ // Avoid the query if it looks OK.
+ _, pathMajor, ok := module.SplitPathVersion(path)
+ if !ok {
+ return "", &module.ModuleError{
+ Path: path,
+ Err: &module.InvalidVersionError{
+ Version: vers,
+ Err: fmt.Errorf("malformed module path %q", path),
+ },
+ }
+ }
+ if vers != "" && module.CanonicalVersion(vers) == vers {
+ if err := module.CheckPathMajor(vers, pathMajor); err == nil {
+ return vers, nil
+ }
+ }
+
+ info, err := Query(path, vers, "", nil)
+ if err != nil {
+ return "", err
+ }
+ return info.Version, nil
+ }
+}
+
// AllowMissingModuleImports allows import paths to be resolved to modules
// when there is no module root. Normally, this is forbidden because it's slow
// and there's no way to make the result reproducible, but some commands
if fi, err := os.Stat(filepath.Join(modRoot, "vendor")); err == nil && fi.IsDir() {
modGo := "unspecified"
- if modFile.Go != nil {
- if semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0 {
+ if index.goVersion != "" {
+ if semver.Compare("v"+index.goVersion, "v1.14") >= 0 {
// The Go version is at least 1.14, and a vendor directory exists.
// Set -mod=vendor by default.
cfg.BuildMod = "vendor"
cfg.BuildModReason = "Go version in go.mod is at least 1.14 and vendor directory exists."
return
} else {
- modGo = modFile.Go.Version
+ modGo = index.goVersion
}
}
}
}
- explicitInGoMod := make(map[module.Version]bool, len(modFile.Require))
for _, r := range modFile.Require {
- explicitInGoMod[r.Mod] = true
if !vendorMeta[r.Mod].Explicit {
if pre114 {
// Before 1.14, modules.txt did not indicate whether modules were listed
// 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
- goModReplacement := make(map[module.Version]module.Version, len(modFile.Replace))
for _, r := range modFile.Replace {
- goModReplacement[r.Old] = r.New
vr := vendorMeta[r.Old].Replacement
if vr == (module.Version{}) {
if pre114 && (r.Old.Version == "" || vendorVersion[r.Old.Path] != r.Old.Version) {
for _, mod := range vendorList {
meta := vendorMeta[mod]
- if meta.Explicit && !explicitInGoMod[mod] {
- vendErrorf(mod, "is marked as explicit in vendor/modules.txt, but not explicitly required in go.mod")
+ if meta.Explicit {
+ if _, inGoMod := index.require[mod]; !inGoMod {
+ vendErrorf(mod, "is marked as explicit in vendor/modules.txt, but not explicitly required in go.mod")
+ }
}
}
for _, mod := range vendorReplaced {
- r, ok := goModReplacement[mod]
- if !ok {
- r, ok = goModReplacement[module.Version{Path: mod.Path}]
- }
- if !ok {
+ r := Replacement(mod)
+ if r == (module.Version{}) {
vendErrorf(mod, "is marked as replaced in vendor/modules.txt, but not replaced in go.mod")
continue
}
// Allowed reports whether module m is allowed (not excluded) by the main module's go.mod.
func Allowed(m module.Version) bool {
- return !excluded[m]
+ return index == nil || !index.exclude[m]
}
func legacyModInit() {
allowWriteGoMod = true
}
-// MinReqs returns a Reqs with minimal dependencies of Target,
+// MinReqs returns a Reqs with minimal additional dependencies of Target,
// as will be written to go.mod.
func MinReqs() mvs.Reqs {
- var direct []string
+ var retain []string
for _, m := range buildList[1:] {
- if loaded.direct[m.Path] {
- direct = append(direct, m.Path)
+ _, explicit := index.require[m]
+ if explicit || loaded.direct[m.Path] {
+ retain = append(retain, m.Path)
}
}
- min, err := mvs.Req(Target, direct, Reqs())
+ min, err := mvs.Req(Target, retain, Reqs())
if err != nil {
base.Fatalf("go: %v", err)
}
return
}
- addGoStmt()
+ if cfg.BuildMod != "readonly" {
+ addGoStmt()
+ }
if loaded != nil {
reqs := MinReqs()
}
modFile.SetRequire(list)
}
+ modFile.Cleanup()
- modFile.Cleanup() // clean file after edits
- new, err := modFile.Format()
- if err != nil {
- base.Fatalf("go: %v", err)
- }
-
- dirty := !bytes.Equal(new, modFileData)
+ dirty := index.modFileIsDirty(modFile)
if dirty && cfg.BuildMod == "readonly" {
// If we're about to fail due to -mod=readonly,
// prefer to report a dirty go.mod over a dirty go.sum
// downloaded modules that we didn't have before.
modfetch.WriteGoSum()
- if !dirty {
- // We don't need to modify go.mod from what we read previously.
+ if !dirty && cfg.CmdName != "mod tidy" {
+ // The go.mod file has the same semantic content that it had before
+ // (but not necessarily the same exact bytes).
// Ignore any intervening edits.
return
}
+ new, err := modFile.Format()
+ if err != nil {
+ base.Fatalf("go: %v", err)
+ }
+ defer func() {
+ // At this point we have determined to make the go.mod file on disk equal to new.
+ index = indexModFile(new, modFile, false)
+ }()
+
unlock := modfetch.SideLock()
defer unlock()
file := ModFilePath()
old, err := renameio.ReadFile(file)
- if !bytes.Equal(old, modFileData) {
- if bytes.Equal(old, new) {
- // Some other process wrote the same go.mod file that we were about to write.
- modFileData = new
- return
- }
+ if bytes.Equal(old, new) {
+ // The go.mod file is already equal to new, possibly as the result of some
+ // other process.
+ return
+ }
+
+ if index != nil && !bytes.Equal(old, index.data) {
if err != nil {
base.Fatalf("go: can't determine whether go.mod has changed: %v", err)
}
if err := renameio.WriteFile(file, new, 0666); err != nil {
base.Fatalf("error writing go.mod: %v", err)
}
- modFileData = new
}
-func fixVersion(path, vers string) (string, error) {
- // Special case: remove the old -gopkgin- hack.
- if strings.HasPrefix(path, "gopkg.in/") && strings.Contains(vers, "-gopkgin-") {
- vers = vers[strings.Index(vers, "-gopkgin-")+len("-gopkgin-"):]
+// 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
}
- // fixVersion is called speculatively on every
- // module, version pair from every go.mod file.
- // Avoid the query if it looks OK.
- _, pathMajor, ok := module.SplitPathVersion(path)
- if !ok {
- return "", &module.ModuleError{
- Path: path,
- Err: &module.InvalidVersionError{
- Version: vers,
- Err: fmt.Errorf("malformed module path %q", path),
- },
+ i.goVersion = ""
+ if modFile.Go != nil {
+ i.goVersion = 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
}
- if vers != "" && module.CanonicalVersion(vers) == vers {
- if err := module.CheckPathMajor(vers, pathMajor); err == nil {
- return vers, nil
+
+ 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
}
- info, err := Query(path, vers, "", nil)
- if err != nil {
- return "", err
+ if modFile.Go == nil {
+ if i.goVersion != "" {
+ return true
+ }
+ } else if modFile.Go.Version != i.goVersion {
+ if i.goVersion == "" && 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
+ }
}
- return info.Version, nil
+
+ for _, x := range modFile.Exclude {
+ if !i.exclude[x.Mod] {
+ return true
+ }
+ }
+
+ return false
}
buildList = append([]module.Version{}, list...)
}
+// TidyBuildList trims the build list to the minimal requirements needed to
+// retain the same versions of all packages from the preceding Load* or
+// ImportPaths* call.
+func TidyBuildList() {
+ used := map[module.Version]bool{Target: true}
+ for _, pkg := range loaded.pkgs {
+ used[pkg.mod] = true
+ }
+
+ keep := []module.Version{Target}
+ var direct []string
+ for _, m := range buildList[1:] {
+ if used[m] {
+ keep = append(keep, m)
+ if loaded.direct[m.Path] {
+ direct = append(direct, m.Path)
+ }
+ } else if cfg.BuildV {
+ if _, ok := index.require[m]; ok {
+ fmt.Fprintf(os.Stderr, "unused %s\n", m.Path)
+ }
+ }
+ }
+
+ min, err := mvs.Req(Target, direct, &mvsReqs{buildList: keep})
+ if err != nil {
+ base.Fatalf("go: %v", err)
+ }
+ buildList = append([]module.Version{Target}, min...)
+}
+
// ImportMap returns the actual package import path
// for an import path found in source code.
// If the given import path does not appear in the source code
// If there is no replacement for mod, Replacement returns
// a module.Version with Path == "".
func Replacement(mod module.Version) module.Version {
- if modFile == nil {
- // Happens during testing and if invoking 'go get' or 'go list' outside a module.
- return module.Version{}
- }
-
- var found *modfile.Replace
- for _, r := range modFile.Replace {
- if r.Old.Path == mod.Path && (r.Old.Version == "" || r.Old.Version == mod.Version) {
- found = r // keep going
+ 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
}
}
- if found == nil {
- return module.Version{}
- }
- return found.New
+ return module.Version{}
}
// mvsReqs implements mvs.Reqs for module semantic versions,
return cached{nil, err}
}
for i, mv := range list {
- for excluded[mv] {
- mv1, err := r.next(mv)
- if err != nil {
- return cached{nil, err}
- }
- if mv1.Version == "none" {
- return cached{nil, fmt.Errorf("%s(%s) depends on excluded %s(%s) with no newer version available", mod.Path, mod.Version, mv.Path, mv.Version)}
+ if index != nil {
+ for index.exclude[mv] {
+ mv1, err := r.next(mv)
+ if err != nil {
+ return cached{nil, err}
+ }
+ if mv1.Version == "none" {
+ return cached{nil, fmt.Errorf("%s(%s) depends on excluded %s(%s) with no newer version available", mod.Path, mod.Version, mv.Path, mv.Version)}
+ }
+ mv = mv1
}
- mv = mv1
}
list[i] = mv
}