package modload
import (
- "cmd/go/internal/base"
- "cmd/go/internal/cfg"
- "cmd/go/internal/gover"
- "cmd/go/internal/mvs"
- "cmd/go/internal/par"
- "cmd/go/internal/slices"
"context"
"errors"
"fmt"
"reflect"
"runtime"
"runtime/debug"
+ "slices"
"strings"
"sync"
"sync/atomic"
+ "cmd/go/internal/base"
+ "cmd/go/internal/cfg"
+ "cmd/go/internal/gover"
+ "cmd/go/internal/mvs"
+ "cmd/go/internal/par"
+
"golang.org/x/mod/module"
)
// accept and/or return an explicit parameter.
var requirements *Requirements
+func mustHaveGoRoot(roots []module.Version) {
+ for _, m := range roots {
+ if m.Path == "go" {
+ return
+ }
+ }
+ panic("go: internal error: missing go root module")
+}
+
// newRequirements returns a new requirement set with the given root modules.
// The dependencies of the roots will be loaded lazily at the first call to the
// Graph method.
// If vendoring is in effect, the caller must invoke initVendor on the returned
// *Requirements before any other method.
func newRequirements(pruning modPruning, rootModules []module.Version, direct map[string]bool) *Requirements {
+ mustHaveGoRoot(rootModules)
+
if pruning == workspace {
return &Requirements{
pruning: pruning,
if workFilePath != "" && pruning != workspace {
panic("in workspace mode, but pruning is not workspace in newRequirements")
}
-
for i, m := range rootModules {
if m.Version == "" && MainModules.Contains(m.Path) {
panic(fmt.Sprintf("newRequirements called with untrimmed build list: rootModules[%v] is a main module", i))
if m.Path == "" || m.Version == "" {
panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
}
- if i > 0 {
- prev := rootModules[i-1]
- if prev.Path > m.Path || (prev.Path == m.Path && gover.ModCompare(m.Path, prev.Version, m.Version) > 0) {
- panic(fmt.Sprintf("newRequirements called with unsorted roots: %v", rootModules))
- }
- }
+ }
+
+ // Allow unsorted root modules, because go and toolchain
+ // are treated as the final graph roots but not trimmed from the build list,
+ // so they always appear at the beginning of the list.
+ r := slices.Clip(slices.Clone(rootModules))
+ gover.ModSort(r)
+ if !reflect.DeepEqual(r, rootModules) {
+ fmt.Fprintln(os.Stderr, "RM", rootModules)
+ panic("unsorted")
}
rs := &Requirements{
pruning: pruning,
- rootModules: slices.Clip(rootModules),
+ rootModules: rootModules,
maxRootVersion: make(map[string]string, len(rootModules)),
direct: direct,
}
func (rs *Requirements) initVendor(vendorList []module.Version) {
rs.graphOnce.Do(func() {
mg := &ModuleGraph{
- g: mvs.NewGraph(cmpVersion, MainModules.GraphRoots()),
+ g: mvs.NewGraph(cmpVersion, MainModules.Versions()),
}
if MainModules.Len() != 1 {
// Unlike LoadModGraph, readModGraph does not attempt to diagnose or update
// inconsistent roots.
func readModGraph(ctx context.Context, pruning modPruning, roots []module.Version, unprune map[module.Version]bool) (*ModuleGraph, error) {
+ mustHaveGoRoot(roots)
if pruning == pruned {
// Enable diagnostics for lazy module loading
// (https://golang.org/ref/mod#lazy-loading) only if the module graph is
})
}
+ var graphRoots []module.Version
+ if inWorkspaceMode() {
+ graphRoots = roots
+ } else {
+ graphRoots = MainModules.Versions()
+ }
var (
mu sync.Mutex // guards mg.g and hasError during loading
hasError bool
mg = &ModuleGraph{
- g: mvs.NewGraph(cmpVersion, MainModules.GraphRoots()),
+ g: mvs.NewGraph(cmpVersion, graphRoots),
}
)
+
if pruning != workspace {
if inWorkspaceMode() {
panic("pruning is not workspace in workspace mode")
})
}
+ mustHaveGoRoot(roots)
for _, m := range roots {
enqueue(m, pruning)
}
roots = append(roots, module.Version{Path: "go", Version: v})
pathIsRoot["go"] = true
}
+ if v, ok := old.rootSelected("toolchain"); ok {
+ roots = append(roots, module.Version{Path: "toolchain", Version: v})
+ pathIsRoot["toolchain"] = true
+ }
// We start by adding roots for every package in "all".
//
// Once that is done, we may still need to add more roots to cover upgraded or
)
if v, ok := old.rootSelected("go"); ok {
keep = append(keep, module.Version{Path: "go", Version: v})
+ keptPath["go"] = true
+ }
+ if v, ok := old.rootSelected("toolchain"); ok {
+ keep = append(keep, module.Version{Path: "toolchain", Version: v})
+ keptPath["toolchain"] = true
}
for _, pkg := range pkgs {
if !pkg.fromExternalModule() {
// versions are the module.Version values of each of the main modules.
// For each of them, the Path fields are ordinary module paths and the Version
// fields are empty strings.
+ // versions is clipped (len=cap).
versions []module.Version
// modRoot maps each module in versions to its absolute filesystem path.
modContainingCWD module.Version
- workFileGoVersion string
+ workFile *modfile.WorkFile
workFileReplaceMap map[module.Version]module.Version
// highest replaced version of each module path; empty string for wildcard-only replacements
return mms.versions
}
-// GraphRoots returns the graph roots for the main module set.
-// Callers should not modify the returned slice.
-// This function is the same as Versions except that in workspace
-// mode it adds a "go" version from the go.work file.
-func (mms *MainModuleSet) GraphRoots() []module.Version {
- versions := mms.Versions()
- if inWorkspaceMode() {
- versions = append(slices.Clip(versions), module.Version{Path: "go", Version: mms.GoVersion()})
- }
- return versions
-}
-
func (mms *MainModuleSet) Contains(path string) bool {
if mms == nil {
return false
// GoVersion returns the go version set on the single module, in module mode,
// or the go.work file in workspace mode.
func (mms *MainModuleSet) GoVersion() string {
- switch {
- case inWorkspaceMode():
- v := mms.workFileGoVersion
- if v == "" {
- // Fall back to 1.18 for go.work files.
- v = "1.18"
- }
- return v
- case mms == nil || len(mms.versions) == 0:
- return "1.18"
- default:
- return modFileGoVersion(mms.ModFile(mms.mustGetSingleMainModule()))
+ if inWorkspaceMode() {
+ if mms.workFile != nil && mms.workFile.Go != nil {
+ return mms.workFile.Go.Version
+ }
+ return defaultGoWorkVersion
+ }
+ if mms != nil && len(mms.versions) == 1 {
+ f := mms.ModFile(mms.mustGetSingleMainModule())
+ if f == nil {
+ // Special case: we are outside a module, like 'go run x.go'.
+ // Assume the local Go version.
+ // TODO(#49228): Clean this up; see loadModFile.
+ return gover.Local()
+ }
+ if f.Go != nil {
+ return f.Go.Version
+ }
}
+ return defaultGoModVersion
+}
+
+// Toolchain returns the toolchain set on the single module, in module mode,
+// or the go.work file in workspace mode.
+func (mms *MainModuleSet) Toolchain() string {
+ if inWorkspaceMode() {
+ if mms.workFile != nil && mms.workFile.Toolchain != nil {
+ return mms.workFile.Toolchain.Name
+ }
+ return "go" + mms.GoVersion()
+ }
+ if mms != nil && len(mms.versions) == 1 {
+ f := mms.ModFile(mms.mustGetSingleMainModule())
+ if f == nil {
+ // Special case: we are outside a module, like 'go run x.go'.
+ // Assume the local Go version.
+ // TODO(#49228): Clean this up; see loadModFile.
+ return gover.LocalToolchain()
+ }
+ if f.Toolchain != nil {
+ return f.Toolchain.Name
+ }
+ }
+ return "go" + mms.GoVersion()
}
func (mms *MainModuleSet) WorkFileReplaceMap() map[module.Version]module.Version {
// make MainModules.Len() == 0 mean that we're in module mode but not inside
// any module.
mainModule := module.Version{Path: "command-line-arguments"}
- MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, "", nil)
+ MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, nil)
goVersion := gover.Local()
rawGoVersion.Store(mainModule, goVersion)
pruning := pruningForGoVersion(goVersion)
if inWorkspaceMode() {
pruning = workspace
}
- requirements = newRequirements(pruning, nil, nil)
+ roots := []module.Version{
+ {Path: "go", Version: gover.Local()},
+ {Path: "toolchain", Version: gover.LocalToolchain()},
+ }
+ requirements = newRequirements(pruning, roots, nil)
if cfg.BuildMod == "vendor" {
// For issue 56536: Some users may have GOFLAGS=-mod=vendor set.
// Make sure it behaves as though the fake module is vendored
}
}
- var wfGoVersion string
- var wfReplace []*modfile.Replace
- if workFile != nil && workFile.Go != nil {
- wfGoVersion = workFile.Go.Version
- }
- if workFile != nil {
- wfReplace = workFile.Replace
- }
- MainModules = makeMainModules(mainModules, modRoots, modFiles, indices, wfGoVersion, wfReplace)
+ MainModules = makeMainModules(mainModules, modRoots, modFiles, indices, workFile)
setDefaultBuildMod() // possibly enable automatic vendoring
rs := requirementsFromModFiles(ctx, workFile, modFiles, opts)
}
}
} else {
- rawGoVersion.Store(mainModule, modFileGoVersion(MainModules.ModFile(mainModule)))
+ rawGoVersion.Store(mainModule, defaultGoModVersion)
}
}
fmt.Fprintf(os.Stderr, "go: creating new go.mod: module %s\n", modPath)
modFile := new(modfile.File)
modFile.AddModuleStmt(modPath)
- MainModules = makeMainModules([]module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil}, "", nil)
+ MainModules = makeMainModules([]module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil}, nil)
addGoStmt(modFile, modFile.Module.Mod, gover.Local()) // Add the go directive before converted module requirements.
convertedFrom, err := convertLegacyConfig(modFile, modRoot)
// makeMainModules creates a MainModuleSet and associated variables according to
// the given main modules.
-func makeMainModules(ms []module.Version, rootDirs []string, modFiles []*modfile.File, indices []*modFileIndex, workFileGoVersion string, workFileReplaces []*modfile.Replace) *MainModuleSet {
+func makeMainModules(ms []module.Version, rootDirs []string, modFiles []*modfile.File, indices []*modFileIndex, workFile *modfile.WorkFile) *MainModuleSet {
for _, m := range ms {
if m.Version != "" {
panic("mainModulesCalled with module.Version with non empty Version field: " + fmt.Sprintf("%#v", m))
}
modRootContainingCWD := findModuleRoot(base.Cwd())
mainModules := &MainModuleSet{
- versions: slices.Clip(ms),
- inGorootSrc: map[module.Version]bool{},
- pathPrefix: map[module.Version]string{},
- modRoot: map[module.Version]string{},
- modFiles: map[module.Version]*modfile.File{},
- indices: map[module.Version]*modFileIndex{},
- workFileGoVersion: workFileGoVersion,
- workFileReplaceMap: toReplaceMap(workFileReplaces),
- highestReplaced: map[string]string{},
+ versions: slices.Clip(ms),
+ inGorootSrc: map[module.Version]bool{},
+ pathPrefix: map[module.Version]string{},
+ modRoot: map[module.Version]string{},
+ modFiles: map[module.Version]*modfile.File{},
+ indices: map[module.Version]*modFileIndex{},
+ highestReplaced: map[string]string{},
+ workFile: workFile,
+ }
+ var workFileReplaces []*modfile.Replace
+ if workFile != nil {
+ workFileReplaces = workFile.Replace
+ mainModules.workFileReplaceMap = toReplaceMap(workFile.Replace)
}
mainModulePaths := make(map[string]bool)
for _, m := range ms {
var roots []module.Version
direct := map[string]bool{}
var pruning modPruning
+ var goVersion, toolchain string
if inWorkspaceMode() {
pruning = workspace
roots = make([]module.Version, len(MainModules.Versions()), 2+len(MainModules.Versions()))
copy(roots, MainModules.Versions())
- // Note: Ignoring the 'go' line in the main modules during mod tidy. See note below.
- if workFile.Go != nil && (opts == nil || !opts.TidyGo) {
- roots = append(roots, module.Version{Path: "go", Version: workFile.Go.Version})
- direct["go"] = true
+ if workFile.Go != nil {
+ goVersion = workFile.Go.Version
+ }
+ if workFile.Toolchain != nil {
+ toolchain = workFile.Toolchain.Name
}
- // Do not add toolchain to roots.
- // We only want to see it in roots if it is on the command line.
} else {
pruning = pruningForGoVersion(MainModules.GoVersion())
if len(modFiles) != 1 {
direct[r.Mod.Path] = true
}
}
- // Note: Ignoring the 'go' line in the main modules during mod tidy -go=
- // so that we can find out the implied minimum go line from the
- // dependencies instead. If it is higher than the -go= flag, we report an error in LoadPackages.
- if modFile.Go != nil && (opts == nil || !opts.TidyGo) {
- roots = append(roots, module.Version{Path: "go", Version: modFile.Go.Version})
- direct["go"] = true
+ if modFile.Go != nil {
+ goVersion = modFile.Go.Version
+ }
+ if modFile.Toolchain != nil {
+ toolchain = modFile.Toolchain.Name
}
- // Do not add "toolchain" to roots.
- // We only want to see it in roots if it is on the command line.
}
+
+ // Add explicit go and toolchain versions, inferring as needed.
+ if opts != nil && opts.TidyGo {
+ goVersion = opts.GoVersion
+ }
+ if goVersion == "" {
+ goVersion = defaultGoModVersion
+ }
+ roots = append(roots, module.Version{Path: "go", Version: goVersion})
+ direct["go"] = true
+
+ if toolchain == "" {
+ toolchain = "go" + goVersion
+ }
+ roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
+ direct["toolchain"] = true
+
gover.ModSort(roots)
rs := newRequirements(pruning, roots, direct)
return rs
// WriteOpts control the behavior of WriteGoMod.
type WriteOpts struct {
+ DropToolchain bool // go get toolchain@none
+ ExplicitToolchain bool // go get has set explicit toolchain version
}
// WriteGoMod writes the current build list back to go.mod.
var list []*modfile.Require
toolchain := ""
- wroteGo := false
+ goVersion := ""
for _, m := range requirements.rootModules {
if m.Path == "go" {
- wroteGo = true
- forceGoStmt(modFile, mainModule, m.Version)
+ goVersion = m.Version
continue
}
if m.Path == "toolchain" {
})
}
- var oldToolchain string
- if modFile.Toolchain != nil {
- oldToolchain = modFile.Toolchain.Name
- }
- oldToolVers := gover.FromToolchain(oldToolchain)
-
- // Update go and toolchain lines.
- toolVers := gover.FromToolchain(toolchain)
-
- // Set go version if missing.
- if modFile.Go == nil || modFile.Go.Version == "" {
- wroteGo = true
- v := modFileGoVersion(modFile)
- if toolVers != "" && gover.Compare(v, toolVers) > 0 {
- v = toolVers
- }
- modFile.AddGoStmt(v)
+ // Update go line.
+ // Every MVS graph we consider should have go as a root,
+ // and toolchain is either implied by the go line or explicitly a root.
+ if goVersion == "" {
+ base.Fatalf("go: internal error: missing go root module in WriteGoMod")
}
- if gover.Compare(modFile.Go.Version, gover.Local()) > 0 {
+ if gover.Compare(goVersion, gover.Local()) > 0 {
// We cannot assume that we know how to update a go.mod to a newer version.
- return &gover.TooNewError{What: "updating go.mod", GoVersion: modFile.Go.Version}
+ return &gover.TooNewError{What: "updating go.mod", GoVersion: goVersion}
}
-
- // If we update the go line and don't have an explicit instruction
- // for what to write in toolchain, make sure toolchain is at least our local version,
- // for reproducibility.
- if wroteGo && toolchain == "" && gover.Compare(oldToolVers, gover.Local()) < 0 && gover.Compare(modFile.Go.Version, GoStrictVersion) >= 0 {
- toolVers = gover.Local()
- toolchain = "go" + toolVers
+ wroteGo := false
+ if modFile.Go == nil || modFile.Go.Version != goVersion {
+ alwaysUpdate := cfg.BuildMod == "mod" || cfg.CmdName == "mod tidy" || cfg.CmdName == "get"
+ if modFile.Go == nil && goVersion == defaultGoModVersion && !alwaysUpdate {
+ // The go.mod has no go line, the implied default Go version matches
+ // what we've computed for the graph, and we're not in one of the
+ // traditional go.mod-updating programs, so leave it alone.
+ } else {
+ wroteGo = true
+ forceGoStmt(modFile, mainModule, goVersion)
+ }
}
-
- // Default to old toolchain.
if toolchain == "" {
- toolchain = oldToolchain
- toolVers = oldToolVers
+ toolchain = "go" + goVersion
}
- if toolchain == "none" {
- toolchain = ""
+
+ // For reproducibility, if we are writing a new go line,
+ // and we're not explicitly modifying the toolchain line with 'go get toolchain@something',
+ // and the toolchain running right now is newer than the current toolchain line,
+ // then update the toolchain line to record the newer toolchain.
+ toolVers := gover.FromToolchain(toolchain)
+ if wroteGo && !opts.DropToolchain && !opts.ExplicitToolchain && gover.Compare(gover.Local(), toolVers) > 0 {
+ toolchain = "go" + gover.Local()
}
- // Remove or add toolchain as needed.
- // If toolchain is older than go version, drop it.
- if toolchain == "" || gover.Compare(modFile.Go.Version, toolVers) >= 0 {
+ if opts.DropToolchain || toolchain == "go"+goVersion {
+ // go get toolchain@none or toolchain matches go line; drop it.
modFile.DropToolchainStmt()
} else {
modFile.AddToolchainStmt(toolchain)
}
// Update require blocks.
- if gover.Compare(modFileGoVersion(modFile), separateIndirectVersion) < 0 {
+ if gover.Compare(goVersion, separateIndirectVersion) < 0 {
modFile.SetRequire(list)
} else {
modFile.SetRequireSeparateIndirect(list)
// tests outside of the main module.
narrowAllVersion = "1.16"
+ // defaultGoModVersion is the Go version to assume for go.mod files
+ // that do not declare a Go version. The go command has been
+ // writing go versions to modules since Go 1.12, so a go.mod
+ // without a version is either very old or recently hand-written.
+ // Since we can't tell which, we have to assume it's very old.
+ // The semantics of the go.mod changed at Go 1.17 to support
+ // graph pruning. If see a go.mod without a go line, we have to
+ // assume Go 1.16 so that we interpret the requirements correctly.
+ // Note that this default must stay at Go 1.16; it cannot be moved forward.
+ defaultGoModVersion = "1.16"
+
+ // defaultGoWorkVersion is the Go version to assume for go.work files
+ // that do not declare a Go version. Workspaces were added in Go 1.18,
+ // so use that.
+ defaultGoWorkVersion = "1.18"
+
// ExplicitIndirectVersion is the Go version at which a
// module's go.mod file is expected to list explicit requirements on every
// module that provides any package transitively imported by that module.
return data, f, err
}
-// modFileGoVersion returns the (non-empty) Go version at which the requirements
-// in modFile are interpreted, or the latest Go version if modFile is nil.
-func modFileGoVersion(modFile *modfile.File) string {
- if modFile == nil {
- return gover.Local()
- }
- if modFile.Go == nil || modFile.Go.Version == "" {
- // The main module necessarily has a go.mod file, and that file lacks a
- // 'go' directive. The 'go' command has been adding that directive
- // automatically since Go 1.12, so this module either dates to Go 1.11 or
- // has been erroneously hand-edited.
- //
- // The semantics of the go.mod file are more-or-less the same from Go 1.11
- // through Go 1.16, changing at 1.17 to support module graph pruning.
- // So even though a go.mod file without a 'go' directive is theoretically a
- // Go 1.11 file, scripts may assume that it ends up as a Go 1.16 module.
- return "1.16"
- }
- return modFile.Go.Version
-}
-
// A modFileIndex is an index of data corresponding to a modFile
// at a specific point in time.
type modFileIndex struct {
toolchain = modFile.Toolchain.Name
}
- // 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
- // and want to bias toward providing useful behavior.
- // go lines are required if we need to declare version 1.17 or later.
- // Note that as of CL 303229, a missing go directive implies 1.16,
- // not “the latest Go version”.
- if goV != i.goVersion && i.goVersion == "" && cfg.BuildMod != "mod" && gover.Compare(goV, "1.17") < 0 {
- goV = ""
- if toolchain != i.toolchain && i.toolchain == "" {
- toolchain = ""
- }
- }
-
if goV != i.goVersion ||
toolchain != i.toolchain ||
len(modFile.Require) != len(i.require) ||
if gover.IsToolchain(m.Path) {
if m.Path == "go" {
// Declare that go 1.2.3 requires toolchain 1.2.3,
- // so that go get knows that downgrading toolchain implies downgrading go.
+ // so that go get knows that downgrading toolchain implies downgrading go
+ // and similarly upgrading go requires upgrading the toolchain.
return &modFileSummary{module: m, require: []module.Version{{Path: "toolchain", Version: "go" + m.Version}}}, nil
}
return &modFileSummary{module: m}, nil