summaryrefslogtreecommitdiff
path: root/libgo/go/cmd/go/internal/modload/init.go
diff options
context:
space:
mode:
Diffstat (limited to 'libgo/go/cmd/go/internal/modload/init.go')
-rw-r--r--libgo/go/cmd/go/internal/modload/init.go617
1 files changed, 477 insertions, 140 deletions
diff --git a/libgo/go/cmd/go/internal/modload/init.go b/libgo/go/cmd/go/internal/modload/init.go
index 807ce8d5dc5..61cbdf2c543 100644
--- a/libgo/go/cmd/go/internal/modload/init.go
+++ b/libgo/go/cmd/go/internal/modload/init.go
@@ -7,6 +7,7 @@ package modload
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"go/build"
"internal/lazyregexp"
@@ -22,26 +23,24 @@ import (
"cmd/go/internal/cache"
"cmd/go/internal/cfg"
"cmd/go/internal/load"
+ "cmd/go/internal/lockedfile"
"cmd/go/internal/modconv"
"cmd/go/internal/modfetch"
"cmd/go/internal/modfetch/codehost"
- "cmd/go/internal/modfile"
- "cmd/go/internal/module"
"cmd/go/internal/mvs"
- "cmd/go/internal/renameio"
"cmd/go/internal/search"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/mod/module"
+ "golang.org/x/mod/semver"
)
var (
- cwd string // TODO(bcmills): Is this redundant with base.Cwd?
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
@@ -56,8 +55,31 @@ var (
CmdModInit bool // running 'go mod init'
CmdModModule string // module argument for 'go mod init'
+
+ 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,
@@ -89,6 +111,9 @@ func Init() {
}
initialized = true
+ // Keep in sync with WillBeEnabled. We perform extra validation here, and
+ // there are lots of diagnostics and side effects, so we can't use
+ // WillBeEnabled directly.
env := cfg.Getenv("GO111MODULE")
switch env {
default:
@@ -129,18 +154,15 @@ func Init() {
os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
}
- var err error
- cwd, err = os.Getwd()
- if err != nil {
- base.Fatalf("go: %v", err)
- }
-
if CmdModInit {
// Running 'go mod init': go.mod will be created in current directory.
- modRoot = cwd
+ modRoot = base.Cwd
} else {
- modRoot = findModuleRoot(cwd)
+ modRoot = findModuleRoot(base.Cwd)
if modRoot == "" {
+ if cfg.ModFile != "" {
+ base.Fatalf("go: cannot find main module, but -modfile was set.\n\t-modfile cannot be used to set the module root directory.")
+ }
if !mustUseModules {
// GO111MODULE is 'auto', and we can't find a module root.
// Stay in GOPATH mode.
@@ -156,6 +178,9 @@ func Init() {
fmt.Fprintf(os.Stderr, "go: warning: ignoring go.mod in system temp root %v\n", os.TempDir())
}
}
+ if cfg.ModFile != "" && !strings.HasSuffix(cfg.ModFile, ".mod") {
+ base.Fatalf("go: -modfile=%s: file does not have .mod extension", cfg.ModFile)
+ }
// We're in module mode. Install the hooks to make it work.
@@ -198,30 +223,23 @@ func Init() {
if modRoot == "" {
// We're in module mode, but not inside a module.
//
- // If the command is 'go get' or 'go list' and all of the args are in the
- // same existing module, we could use that module's download directory in
- // the module cache as the module root, applying any replacements and/or
- // exclusions specified by that module. However, that would leave us in a
- // strange state: we want 'go get' to be consistent with 'go list', and 'go
- // list' should be able to operate on multiple modules. Moreover, the 'get'
- // target might specify relative file paths (e.g. in the same repository) as
- // replacements, and we would not be able to apply those anyway: we would
- // need to either error out or ignore just those replacements, when a build
- // from an empty module could proceed without error.
+ // Commands like 'go build', 'go run', 'go list' have no go.mod file to
+ // read or write. They would need to find and download the latest versions
+ // of a potentially large number of modules with no way to save version
+ // information. We can succeed slowly (but not reproducibly), but that's
+ // not usually a good experience.
//
- // Instead, we'll operate as though we're in some ephemeral external module,
- // ignoring all replacements and exclusions uniformly.
-
- // Normally we check sums using the go.sum file from the main module, but
- // without a main module we do not have an authoritative go.sum file.
+ // Instead, we forbid resolving import paths to modules other than std and
+ // cmd. Users may still build packages specified with .go files on the
+ // command line, but they'll see an error if those files import anything
+ // outside std.
//
- // TODO(bcmills): In Go 1.13, check sums when outside the main module.
+ // This can be overridden by calling AllowMissingModuleImports.
+ // For example, 'go get' does this, since it is expected to resolve paths.
//
- // One possible approach is to merge the go.sum files from all of the
- // modules we download: that doesn't protect us against bad top-level
- // modules, but it at least ensures consistency for transitive dependencies.
+ // See golang.org/issue/32027.
} else {
- modfetch.GoSumFile = filepath.Join(modRoot, "go.sum")
+ modfetch.GoSumFile = strings.TrimSuffix(ModFilePath(), ".mod") + ".sum"
search.SetModRoot(modRoot)
}
}
@@ -237,6 +255,54 @@ func init() {
}
}
+// WillBeEnabled checks whether modules should be enabled but does not
+// initialize modules by installing hooks. If Init has already been called,
+// WillBeEnabled returns the same result as Enabled.
+//
+// This function is needed to break a cycle. The main package needs to know
+// whether modules are enabled in order to install the module or GOPATH version
+// of 'go get', but Init reads the -modfile flag in 'go get', so it shouldn't
+// be called until the command is installed and flags are parsed. Instead of
+// calling Init and Enabled, the main package can call this function.
+func WillBeEnabled() bool {
+ if modRoot != "" || mustUseModules {
+ return true
+ }
+ if initialized {
+ return false
+ }
+
+ // Keep in sync with Init. Init does extra validation and prints warnings or
+ // exits, so it can't call this function directly.
+ env := cfg.Getenv("GO111MODULE")
+ switch env {
+ case "on":
+ return true
+ case "auto", "":
+ break
+ default:
+ return false
+ }
+
+ if CmdModInit {
+ // Running 'go mod init': go.mod will be created in current directory.
+ return true
+ }
+ if modRoot := findModuleRoot(base.Cwd); modRoot == "" {
+ // GO111MODULE is 'auto', and we can't find a module root.
+ // Stay in GOPATH mode.
+ return false
+ } else if search.InDir(modRoot, os.TempDir()) == "." {
+ // If you create /tmp/go.mod for experimenting,
+ // then any tests that create work directories under /tmp
+ // will find it and get modules when they're not expecting them.
+ // It's a bit of a peculiar thing to disallow but quite mysterious
+ // when it happens. See golang.org/issue/26708.
+ return false
+ }
+ return true
+}
+
// Enabled reports whether modules are (or must be) enabled.
// If modules are enabled but there is no main module, Enabled returns true
// and then the first use of module information will call die
@@ -263,6 +329,20 @@ func HasModRoot() bool {
return modRoot != ""
}
+// ModFilePath returns the effective path of the go.mod file. Normally, this
+// "go.mod" in the directory returned by ModRoot, but the -modfile flag may
+// change its location. ModFilePath calls base.Fatalf if there is no main
+// module, even if -modfile is set.
+func ModFilePath() string {
+ if !HasModRoot() {
+ die()
+ }
+ if cfg.ModFile != "" {
+ return cfg.ModFile
+ }
+ return filepath.Join(modRoot, "go.mod")
+}
+
// printStackInDie causes die to print a stack trace.
//
// It is enabled by the testgo tag, and helps to diagnose paths that
@@ -276,24 +356,25 @@ func die() {
if cfg.Getenv("GO111MODULE") == "off" {
base.Fatalf("go: modules disabled by GO111MODULE=off; see 'go help modules'")
}
- if cwd != "" {
- if dir, name := findAltConfig(cwd); dir != "" {
- rel, err := filepath.Rel(cwd, dir)
- if err != nil {
- rel = dir
- }
- cdCmd := ""
- if rel != "." {
- cdCmd = fmt.Sprintf("cd %s && ", rel)
- }
- base.Fatalf("go: cannot find main module, but found %s in %s\n\tto create a module there, run:\n\t%sgo mod init", name, dir, cdCmd)
+ if dir, name := findAltConfig(base.Cwd); dir != "" {
+ rel, err := filepath.Rel(base.Cwd, dir)
+ if err != nil {
+ rel = dir
}
+ cdCmd := ""
+ if rel != "." {
+ cdCmd = fmt.Sprintf("cd %s && ", rel)
+ }
+ base.Fatalf("go: cannot find main module, but found %s in %s\n\tto create a module there, run:\n\t%sgo mod init", name, dir, cdCmd)
}
base.Fatalf("go: cannot find main module; see 'go help modules'")
}
// InitMod sets Target and, if there is a main module, parses the initial build
// list from its go.mod file, creating and populating that file if needed.
+//
+// As a side-effect, InitMod sets a default for cfg.BuildMod if it does not
+// already have an explicit value.
func InitMod() {
if len(buildList) > 0 {
return
@@ -315,19 +396,20 @@ func InitMod() {
return
}
- gomod := filepath.Join(modRoot, "go.mod")
- data, err := renameio.ReadFile(gomod)
+ gomod := ModFilePath()
+ data, err := lockedfile.Read(gomod)
if err != nil {
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.
@@ -344,20 +426,77 @@ func InitMod() {
legacyModInit()
}
- excluded = make(map[module.Version]bool)
- for _, x := range f.Exclude {
- excluded[x.Mod] = true
- }
modFileToBuildList()
- stdVendorMode()
- WriteGoMod()
+ setDefaultBuildMod()
+ if cfg.BuildMod == "vendor" {
+ readVendorList()
+ checkVendorConsistency()
+ } else {
+ // TODO(golang.org/issue/33326): if cfg.BuildMod != "readonly"?
+ WriteGoMod()
+ }
+}
+
+// fixVersion returns a modfile.VersionFixer implemented using the Query function.
+//
+// It resolves commit hashes and branch names to versions,
+// canonicalizes versions 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
+// like 'go get' are expected to do this.
+func AllowMissingModuleImports() {
+ allowMissingModuleImports = true
}
// modFileToBuildList initializes buildList from the modFile.
func modFileToBuildList() {
Target = modFile.Module.Mod
targetPrefix = Target.Path
- if rel := search.InDir(cwd, cfg.GOROOTsrc); rel != "" {
+ if rel := search.InDir(base.Cwd, cfg.GOROOTsrc); rel != "" {
targetInGorootSrc = true
if Target.Path == "std" {
targetPrefix = ""
@@ -371,45 +510,144 @@ func modFileToBuildList() {
buildList = list
}
-// stdVendorMode applies inside $GOROOT/src.
-// It checks that the go.mod matches vendor/modules.txt
-// and then sets -mod=vendor unless this is a command
-// that has to do explicitly with modules.
-func stdVendorMode() {
- if !targetInGorootSrc {
+// setDefaultBuildMod sets a default value for cfg.BuildMod
+// if it is currently empty.
+func setDefaultBuildMod() {
+ if cfg.BuildMod != "" {
+ // Don't override an explicit '-mod=' argument.
return
}
+ cfg.BuildMod = "mod"
if cfg.CmdName == "get" || strings.HasPrefix(cfg.CmdName, "mod ") {
+ // Don't set -mod implicitly for commands whose purpose is to
+ // manipulate the build list.
+ return
+ }
+ if modRoot == "" {
return
}
+ if fi, err := os.Stat(filepath.Join(modRoot, "vendor")); err == nil && fi.IsDir() {
+ modGo := "unspecified"
+ 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 = index.goVersion
+ }
+ }
+
+ // Since a vendor directory exists, we have a non-trivial reason for
+ // choosing -mod=mod, although it probably won't be used for anything.
+ // Record the reason anyway for consistency.
+ // It may be overridden if we switch to mod=readonly below.
+ cfg.BuildModReason = fmt.Sprintf("Go version in go.mod is %s.", modGo)
+ }
+
+ p := ModFilePath()
+ if fi, err := os.Stat(p); err == nil && !hasWritePerm(p, fi) {
+ cfg.BuildMod = "readonly"
+ cfg.BuildModReason = "go.mod file is read-only."
+ }
+}
+
+// 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() {
readVendorList()
-BuildList:
- for _, m := range buildList {
- if m.Path == "cmd" || m.Path == "std" {
- continue
+
+ pre114 := false
+ if modFile.Go == nil || semver.Compare("v"+modFile.Go.Version, "v1.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 ...interface{}) {
+ 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)
}
- for _, v := range vendorList {
- if m.Path == v.Path {
- if m.Version != v.Version {
- base.Fatalf("go: inconsistent vendoring in %s:\n"+
- "\tgo.mod requires %s %s but vendor/modules.txt has %s.\n"+
- "\trun 'go mod tidy; go mod vendor' to sync",
- modRoot, m.Path, m.Version, v.Version)
+ }
+
+ 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))
}
- continue BuildList
+ } 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
+ for _, r := range modFile.Replace {
+ vr := vendorMeta[r.Old].Replacement
+ if vr == (module.Version{}) {
+ 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 go.mod, but not marked as replaced in vendor/modules.txt")
+ }
+ } else if vr != r.New {
+ vendErrorf(r.Old, "is replaced by %s in go.mod, but marked as replaced by %s in vendor/modules.txt", describe(r.New), describe(vr))
+ }
+ }
+
+ for _, mod := range vendorList {
+ meta := vendorMeta[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")
}
}
- base.Fatalf("go: inconsistent vendoring in %s:\n"+
- "\tgo.mod requires %s %s but vendor/modules.txt does not include it.\n"+
- "\trun 'go mod tidy; go mod vendor' to sync", modRoot, m.Path, m.Version)
}
- cfg.BuildMod = "vendor"
+
+ for _, mod := range vendorReplaced {
+ r := Replacement(mod)
+ if r == (module.Version{}) {
+ vendErrorf(mod, "is marked as replaced in vendor/modules.txt, but not replaced in go.mod")
+ continue
+ }
+ if meta := vendorMeta[mod]; r != meta.Replacement {
+ vendErrorf(mod, "is marked as replaced by %s in vendor/modules.txt, but replaced by %s in go.mod", describe(meta.Replacement), describe(r))
+ }
+ }
+
+ if vendErrors.Len() > 0 {
+ base.Fatalf("go: inconsistent vendoring in %s:%s\n\nrun 'go mod vendor' to sync, or use -mod=mod or -mod=readonly to ignore the vendor directory", modRoot, vendErrors)
+ }
}
// 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() {
@@ -478,6 +716,9 @@ var altConfigs = []string{
}
func findModuleRoot(dir string) (root string) {
+ if dir == "" {
+ panic("dir not set")
+ }
dir = filepath.Clean(dir)
// Look for enclosing go.mod.
@@ -495,6 +736,9 @@ func findModuleRoot(dir string) (root string) {
}
func findAltConfig(dir string) (root, name string) {
+ if dir == "" {
+ panic("dir not set")
+ }
dir = filepath.Clean(dir)
for {
for _, name := range altConfigs {
@@ -625,16 +869,17 @@ func AllowWriteGoMod() {
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, buildList, direct, Reqs())
+ min, err := mvs.Req(Target, retain, Reqs())
if err != nil {
base.Fatalf("go: %v", err)
}
@@ -655,7 +900,9 @@ func WriteGoMod() {
return
}
- addGoStmt()
+ if cfg.BuildMod != "readonly" {
+ addGoStmt()
+ }
if loaded != nil {
reqs := MinReqs()
@@ -672,87 +919,177 @@ func WriteGoMod() {
}
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
- base.Fatalf("go: updates to go.mod needed, disabled by -mod=readonly")
+ if cfg.BuildModReason != "" {
+ base.Fatalf("go: updates to go.mod needed, disabled by -mod=readonly\n\t(%s)", cfg.BuildModReason)
+ } else {
+ base.Fatalf("go: updates to go.mod needed, disabled by -mod=readonly")
+ }
}
// Always update go.sum, even if we didn't change go.mod: we may have
// 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
}
- unlock := modfetch.SideLock()
- defer unlock()
+ 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)
+ }()
+
+ // Make a best-effort attempt to acquire the side lock, only to exclude
+ // previous versions of the 'go' command from making simultaneous edits.
+ if unlock, err := modfetch.SideLock(); err == nil {
+ defer unlock()
+ }
- file := filepath.Join(modRoot, "go.mod")
- old, err := renameio.ReadFile(file)
- if !bytes.Equal(old, modFileData) {
+ errNoChange := errors.New("no update needed")
+
+ err = lockedfile.Transform(ModFilePath(), func(old []byte) ([]byte, error) {
if bytes.Equal(old, new) {
- // Some other process wrote the same go.mod file that we were about to write.
- modFileData = new
- return
+ // The go.mod file is already equal to new, possibly as the result of some
+ // other process.
+ return nil, errNoChange
}
- if err != nil {
- base.Fatalf("go: can't determine whether go.mod has changed: %v", err)
+
+ if index != nil && !bytes.Equal(old, index.data) {
+ // The contents of the go.mod file have changed. In theory we could add all
+ // of the new modules to the build list, recompute, and check whether any
+ // module in *our* build list got bumped to a different version, but that's
+ // a lot of work for marginal benefit. Instead, fail the command: if users
+ // want to run concurrent commands, they need to start with a complete,
+ // consistent module definition.
+ return nil, fmt.Errorf("existing contents have changed since last read")
}
- // The contents of the go.mod file have changed. In theory we could add all
- // of the new modules to the build list, recompute, and check whether any
- // module in *our* build list got bumped to a different version, but that's
- // a lot of work for marginal benefit. Instead, fail the command: if users
- // want to run concurrent commands, they need to start with a complete,
- // consistent module definition.
- base.Fatalf("go: updates to go.mod needed, but contents have changed")
+ return new, nil
+ })
+
+ if err != nil && err != errNoChange {
+ base.Fatalf("go: updating go.mod: %v", err)
+ }
+}
+
+// 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.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
}
- if err := renameio.WriteFile(file, new, 0666); err != nil {
- base.Fatalf("error writing go.mod: %v", err)
+ i.exclude = make(map[module.Version]bool, len(modFile.Exclude))
+ for _, x := range modFile.Exclude {
+ i.exclude[x.Mod] = true
}
- modFileData = new
+
+ return i
}
-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-"):]
+// 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
}
- // 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 modFile.Module == nil {
+ if i.module != (module.Version{}) {
+ return true
}
+ } else if modFile.Module.Mod != i.module {
+ return true
}
- if vers != "" && module.CanonicalVersion(vers) == vers {
- if err := module.MatchPathMajor(vers, pathMajor); err == nil {
- return vers, nil
+
+ 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
}
}
- info, err := Query(path, vers, "", nil)
- if err != nil {
- return "", err
+ 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
}