]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/cmd/go/internal/modload/init.go
[dev.cmdgo] all: merge master (9eee0ed) into dev.cmdgo
[gostls13.git] / src / cmd / go / internal / modload / init.go
index a8cbd9fe16d1862d673de85f4f9e301461ab07d3..a3337d6d23aaa3c3ebde7fad88760781e68814c9 100644 (file)
@@ -17,6 +17,7 @@ import (
        "path/filepath"
        "strconv"
        "strings"
+       "sync"
 
        "cmd/go/internal/base"
        "cmd/go/internal/cfg"
@@ -45,26 +46,145 @@ var (
        allowMissingModuleImports bool
 )
 
+func TODOWorkspaces(s string) error {
+       return fmt.Errorf("need to support this for workspaces: %s", s)
+}
+
 // Variables set in Init.
 var (
        initialized bool
-       modRoot     string
-       gopath      string
+
+       // These are primarily used to initialize the MainModules, and should be
+       // eventually superceded by them but are still used in cases where the module
+       // roots are required but MainModules hasn't been initialized yet. Set to
+       // the modRoots of the main modules.
+       // modRoots != nil implies len(modRoots) > 0
+       modRoots []string
+       gopath   string
 )
 
-// Variables set in initTarget (during {Load,Create}ModFile).
+// Variable set in InitWorkfile
 var (
-       Target module.Version
+       // Set to the path to the go.work file, or "" if workspace mode is disabled.
+       workFilePath string
+)
+
+type MainModuleSet struct {
+       // 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 []module.Version
+
+       // modRoot maps each module in versions to its absolute filesystem path.
+       modRoot map[module.Version]string
 
-       // targetPrefix is the path prefix for packages in Target, without a trailing
-       // slash. For most modules, targetPrefix is just Target.Path, but the
+       // pathPrefix is the path prefix for packages in the module, without a trailing
+       // slash. For most modules, pathPrefix is just version.Path, but the
        // standard-library module "std" has an empty prefix.
-       targetPrefix string
+       pathPrefix map[module.Version]string
 
-       // targetInGorootSrc caches whether modRoot is within GOROOT/src.
+       // inGorootSrc caches whether modRoot is within GOROOT/src.
        // The "std" module is special within GOROOT/src, but not otherwise.
-       targetInGorootSrc bool
-)
+       inGorootSrc map[module.Version]bool
+
+       modFiles map[module.Version]*modfile.File
+
+       indexMu sync.Mutex
+       indices map[module.Version]*modFileIndex
+}
+
+func (mms *MainModuleSet) PathPrefix(m module.Version) string {
+       return mms.pathPrefix[m]
+}
+
+// Versions returns 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.
+// Callers should not modify the returned slice.
+func (mms *MainModuleSet) Versions() []module.Version {
+       if mms == nil {
+               return nil
+       }
+       return mms.versions
+}
+
+func (mms *MainModuleSet) Contains(path string) bool {
+       if mms == nil {
+               return false
+       }
+       for _, v := range mms.versions {
+               if v.Path == path {
+                       return true
+               }
+       }
+       return false
+}
+
+func (mms *MainModuleSet) ModRoot(m module.Version) string {
+       _ = TODOWorkspaces(" Do we need the Init? The original modRoot calls it. Audit callers.")
+       Init()
+       if mms == nil {
+               return ""
+       }
+       return mms.modRoot[m]
+}
+
+func (mms *MainModuleSet) InGorootSrc(m module.Version) bool {
+       if mms == nil {
+               return false
+       }
+       return mms.inGorootSrc[m]
+}
+
+func (mms *MainModuleSet) mustGetSingleMainModule() module.Version {
+       if mms == nil || len(mms.versions) == 0 {
+               panic("internal error: mustGetSingleMainModule called in context with no main modules")
+       }
+       if len(mms.versions) != 1 {
+               _ = TODOWorkspaces("Check if we're in workspace mode before returning the below error.")
+               panic("internal error: mustGetSingleMainModule called in workspace mode")
+       }
+       return mms.versions[0]
+}
+
+func (mms *MainModuleSet) GetSingleIndexOrNil() *modFileIndex {
+       if mms == nil {
+               return nil
+       }
+       if len(mms.versions) == 0 {
+               return nil
+       }
+       if len(mms.versions) != 1 {
+               _ = TODOWorkspaces("Check if we're in workspace mode before returning the below error.")
+               panic("internal error: mustGetSingleMainModule called in workspace mode")
+       }
+       return mms.indices[mms.versions[0]]
+}
+
+func (mms *MainModuleSet) Index(m module.Version) *modFileIndex {
+       mms.indexMu.Lock()
+       defer mms.indexMu.Unlock()
+       return mms.indices[m]
+}
+
+func (mms *MainModuleSet) SetIndex(m module.Version, index *modFileIndex) {
+       mms.indexMu.Lock()
+       defer mms.indexMu.Unlock()
+       mms.indices[m] = index
+}
+
+func (mms *MainModuleSet) ModFile(m module.Version) *modfile.File {
+       return mms.modFiles[m]
+}
+
+func (mms *MainModuleSet) Len() int {
+       if mms == nil {
+               return 0
+       }
+       return len(mms.versions)
+}
+
+var MainModules *MainModuleSet
 
 type Root int
 
@@ -94,6 +214,7 @@ const (
 // in go.mod, edit it before loading.
 func ModFile() *modfile.File {
        Init()
+       modFile := MainModules.ModFile(MainModules.mustGetSingleMainModule())
        if modFile == nil {
                die()
        }
@@ -105,6 +226,20 @@ func BinDir() string {
        return filepath.Join(gopath, "bin")
 }
 
+// InitWorkfile initializes the workFilePath variable for commands that
+// operate in workspace mode. It should not be called by other commands,
+// for example 'go mod tidy', that don't operate in workspace mode.
+func InitWorkfile() {
+       switch cfg.WorkFile {
+       case "off":
+               workFilePath = ""
+       case "", "auto":
+               workFilePath = findWorkspaceFile(base.Cwd())
+       default:
+               workFilePath = cfg.WorkFile
+       }
+}
+
 // Init determines whether module mode is enabled, locates the root of the
 // current module (if any), sets environment variables for Git subprocesses, and
 // configures the cfg, codehost, load, modfetch, and search packages for use
@@ -169,18 +304,19 @@ func Init() {
        if os.Getenv("GCM_INTERACTIVE") == "" {
                os.Setenv("GCM_INTERACTIVE", "never")
        }
-
-       if modRoot != "" {
+       if modRoots != nil {
                // modRoot set before Init was called ("go mod init" does this).
                // No need to search for go.mod.
        } else if RootMode == NoRoot {
                if cfg.ModFile != "" && !base.InGOFLAGS("-modfile") {
                        base.Fatalf("go: -modfile cannot be used with commands that ignore the current module")
                }
-               modRoot = ""
+               modRoots = nil
+       } else if inWorkspaceMode() {
+               // We're in workspace mode.
        } else {
-               modRoot = findModuleRoot(base.Cwd())
-               if modRoot == "" {
+               modRoots = findModuleRoots(base.Cwd())
+               if modRoots == nil {
                        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.")
                        }
@@ -192,13 +328,13 @@ func Init() {
                                // Stay in GOPATH mode.
                                return
                        }
-               } else if search.InDir(modRoot, os.TempDir()) == "." {
+               } else if search.InDir(modRoots[0], 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.
-                       modRoot = ""
+                       modRoots = nil
                        fmt.Fprintf(os.Stderr, "go: warning: ignoring go.mod in system temp root %v\n", os.TempDir())
                        if !mustUseModules {
                                return
@@ -212,6 +348,7 @@ func Init() {
        // We're in module mode. Set any global variables that need to be set.
        cfg.ModulesEnabled = true
        setDefaultBuildMod()
+       _ = TODOWorkspaces("ensure that buildmod is readonly")
        list := filepath.SplitList(cfg.BuildContext.GOPATH)
        if len(list) == 0 || list[0] == "" {
                base.Fatalf("missing $GOPATH")
@@ -221,7 +358,18 @@ func Init() {
                base.Fatalf("$GOPATH/go.mod exists but should not")
        }
 
-       if modRoot == "" {
+       if inWorkspaceMode() {
+
+               _ = TODOWorkspaces("go.work.sum, and also allow modfetch to fall back to individual go.sums")
+               _ = TODOWorkspaces("replaces")
+               var err error
+               modRoots, err = loadWorkFile(workFilePath)
+               if err != nil {
+                       base.Fatalf("reading go.work: %v", err)
+               }
+               modfetch.GoSumFile = workFilePath + ".sum"
+               // TODO(matloob) should workRoot just be workFile?
+       } else if modRoots == nil {
                // We're in module mode, but not inside a module.
                //
                // Commands like 'go build', 'go run', 'go list' have no go.mod file to
@@ -240,8 +388,8 @@ func Init() {
                //
                // See golang.org/issue/32027.
        } else {
+               _ = TODOWorkspaces("Instead of modfile path, find modfile OR workfile path depending on mode")
                modfetch.GoSumFile = strings.TrimSuffix(ModFilePath(), ".mod") + ".sum"
-               search.SetModRoot(modRoot)
        }
 }
 
@@ -255,7 +403,7 @@ func Init() {
 // 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 != "" || cfg.ModulesEnabled {
+       if modRoots != nil || cfg.ModulesEnabled {
                // Already enabled.
                return true
        }
@@ -276,11 +424,12 @@ func WillBeEnabled() bool {
                return false
        }
 
-       if modRoot := findModuleRoot(base.Cwd()); modRoot == "" {
+       if modRoots := findModuleRoots(base.Cwd()); modRoots == nil {
                // GO111MODULE is 'auto', and we can't find a module root.
                // Stay in GOPATH mode.
                return false
-       } else if search.InDir(modRoot, os.TempDir()) == "." {
+       } else if search.InDir(modRoots[0], os.TempDir()) == "." {
+               _ = TODOWorkspaces("modRoots[0] is not right here")
                // 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.
@@ -297,7 +446,7 @@ func WillBeEnabled() bool {
 // (usually through MustModRoot).
 func Enabled() bool {
        Init()
-       return modRoot != "" || cfg.ModulesEnabled
+       return modRoots != nil || cfg.ModulesEnabled
 }
 
 // ModRoot returns the root of the main module.
@@ -306,7 +455,22 @@ func ModRoot() string {
        if !HasModRoot() {
                die()
        }
-       return modRoot
+       if inWorkspaceMode() {
+               panic("ModRoot called in workspace mode")
+       }
+       // This is similar to MustGetSingleMainModule but we can't call that
+       // because MainModules may not yet exist when ModRoot is called.
+       if len(modRoots) != 1 {
+               panic("not in workspace mode but there are multiple ModRoots")
+       }
+       return modRoots[0]
+}
+
+func inWorkspaceMode() bool {
+       if !initialized {
+               panic("inWorkspaceMode called before modload.Init called")
+       }
+       return workFilePath != ""
 }
 
 // HasModRoot reports whether a main module is present.
@@ -314,7 +478,7 @@ func ModRoot() string {
 // does not require a main module.
 func HasModRoot() bool {
        Init()
-       return modRoot != ""
+       return modRoots != nil
 }
 
 // ModFilePath returns the effective path of the go.mod file. Normally, this
@@ -322,9 +486,10 @@ func HasModRoot() bool {
 // change its location. ModFilePath calls base.Fatalf if there is no main
 // module, even if -modfile is set.
 func ModFilePath() string {
-       if !HasModRoot() {
-               die()
-       }
+       return modFilePath(ModRoot())
+}
+
+func modFilePath(modRoot string) string {
        if cfg.ModFile != "" {
                return cfg.ModFile
        }
@@ -365,6 +530,31 @@ func (goModDirtyError) Error() string {
 
 var errGoModDirty error = goModDirtyError{}
 
+func loadWorkFile(path string) (modRoots []string, err error) {
+       workDir := filepath.Dir(path)
+       workData, err := lockedfile.Read(path)
+       if err != nil {
+               return nil, err
+       }
+       wf, err := modfile.ParseWork(path, workData, nil)
+       if err != nil {
+               return nil, err
+       }
+       seen := map[string]bool{}
+       for _, d := range wf.Directory {
+               modRoot := d.Path
+               if !filepath.IsAbs(modRoot) {
+                       modRoot = filepath.Join(workDir, modRoot)
+               }
+               if seen[modRoot] {
+                       return nil, fmt.Errorf("path %s appears multiple times in workspace", modRoot)
+               }
+               seen[modRoot] = true
+               modRoots = append(modRoots, modRoot)
+       }
+       return modRoots, nil
+}
+
 // LoadModFile sets Target and, if there is a main module, parses the initial
 // build list from its go.mod file.
 //
@@ -402,78 +592,99 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
        }
 
        Init()
-       if modRoot == "" {
-               Target = module.Version{Path: "command-line-arguments"}
-               targetPrefix = "command-line-arguments"
+       if len(modRoots) == 0 {
+               _ = TODOWorkspaces("Instead of creating a fake module with an empty modroot, 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})
                goVersion := LatestGoVersion()
-               rawGoVersion.Store(Target, goVersion)
+               rawGoVersion.Store(mainModule, goVersion)
                requirements = newRequirements(modDepthFromGoVersion(goVersion), nil, nil)
                return requirements, false
        }
 
-       gomod := ModFilePath()
-       var data []byte
-       var err error
-       if gomodActual, ok := fsys.OverlayPath(gomod); ok {
-               // Don't lock go.mod if it's part of the overlay.
-               // On Plan 9, locking requires chmod, and we don't want to modify any file
-               // in the overlay. See #44700.
-               data, err = os.ReadFile(gomodActual)
-       } else {
-               data, err = lockedfile.Read(gomodActual)
-       }
-       if err != nil {
-               base.Fatalf("go: %v", err)
-       }
+       var modFiles []*modfile.File
+       var mainModules []module.Version
+       var indices []*modFileIndex
+       for _, modroot := range modRoots {
+               gomod := modFilePath(modroot)
+               var data []byte
+               var err error
+               if gomodActual, ok := fsys.OverlayPath(gomod); ok {
+                       // Don't lock go.mod if it's part of the overlay.
+                       // On Plan 9, locking requires chmod, and we don't want to modify any file
+                       // in the overlay. See #44700.
+                       data, err = os.ReadFile(gomodActual)
+               } else {
+                       data, err = lockedfile.Read(gomodActual)
+               }
+               if err != nil {
+                       base.Fatalf("go: %v", err)
+               }
 
-       var fixed bool
-       f, err := modfile.Parse(gomod, data, fixVersion(ctx, &fixed))
-       if err != nil {
-               // Errors returned by modfile.Parse begin with file:line.
-               base.Fatalf("go: errors parsing go.mod:\n%s\n", err)
-       }
-       if f.Module == nil {
-               // No module declaration. Must add module path.
-               base.Fatalf("go: no module declaration in go.mod. To specify the module path:\n\tgo mod edit -module=example.com/mod")
-       }
+               var fixed bool
+               f, err := modfile.Parse(gomod, data, fixVersion(ctx, &fixed))
+               if err != nil {
+                       // Errors returned by modfile.Parse begin with file:line.
+                       base.Fatalf("go: errors parsing go.mod:\n%s\n", err)
+               }
+               if f.Module == nil {
+                       // No module declaration. Must add module path.
+                       base.Fatalf("go: no module declaration in go.mod. To specify the module path:\n\tgo mod edit -module=example.com/mod")
+               }
 
-       modFile = f
-       initTarget(f.Module.Mod)
-       index = indexModFile(data, f, fixed)
+               modFiles = append(modFiles, f)
+               mainModule := f.Module.Mod
+               mainModules = append(mainModules, mainModule)
+               indices = append(indices, indexModFile(data, f, mainModule, fixed))
 
-       if err := module.CheckImportPath(f.Module.Mod.Path); err != nil {
-               if pathErr, ok := err.(*module.InvalidPathError); ok {
-                       pathErr.Kind = "module"
+               if err := module.CheckImportPath(f.Module.Mod.Path); err != nil {
+                       if pathErr, ok := err.(*module.InvalidPathError); ok {
+                               pathErr.Kind = "module"
+                       }
+                       base.Fatalf("go: %v", err)
                }
-               base.Fatalf("go: %v", err)
        }
 
+       MainModules = makeMainModules(mainModules, modRoots, modFiles, indices)
        setDefaultBuildMod() // possibly enable automatic vendoring
-       rs = requirementsFromModFile(ctx)
+       rs = requirementsFromModFiles(ctx, modFiles)
+
+       if inWorkspaceMode() {
+               // We don't need to do anything for vendor or update the mod file so
+               // return early.
+
+               _ = TODOWorkspaces("don't worry about commits for now, but eventually will want to update go.work files")
+               return rs, false
+       }
+
+       mainModule := MainModules.mustGetSingleMainModule()
 
        if cfg.BuildMod == "vendor" {
                readVendorList()
-               checkVendorConsistency()
+               index := MainModules.Index(mainModule)
+               modFile := MainModules.ModFile(mainModule)
+               checkVendorConsistency(index, modFile)
                rs.initVendor(vendorList)
        }
-       if index.goVersionV == "" {
+       if MainModules.Index(mainModule).goVersionV == "" {
                // TODO(#45551): Do something more principled instead of checking
                // cfg.CmdName directly here.
                if cfg.BuildMod == "mod" && cfg.CmdName != "mod graph" && cfg.CmdName != "mod why" {
-                       addGoStmt(LatestGoVersion())
+                       addGoStmt(MainModules.ModFile(mainModule), mainModule, LatestGoVersion())
                        if go117EnableLazyLoading {
                                // We need to add a 'go' version to the go.mod file, but we must assume
                                // that its existing contents match something between Go 1.11 and 1.16.
                                // Go 1.11 through 1.16 have eager requirements, but the latest Go
                                // version uses lazy requirements instead â€” so we need to cnvert the
                                // requirements to be lazy.
+                               var err error
                                rs, err = convertDepth(ctx, rs, lazy)
                                if err != nil {
                                        base.Fatalf("go: %v", err)
                                }
                        }
                } else {
-                       rawGoVersion.Store(Target, modFileGoVersion())
+                       rawGoVersion.Store(mainModule, modFileGoVersion())
                }
        }
 
@@ -491,7 +702,8 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
 // exactly the same as in the legacy configuration (for example, we can't get
 // packages at multiple versions from the same module).
 func CreateModFile(ctx context.Context, modPath string) {
-       modRoot = base.Cwd()
+       modRoot := base.Cwd()
+       modRoots = []string{modRoot}
        Init()
        modFilePath := ModFilePath()
        if _, err := fsys.Stat(modFilePath); err == nil {
@@ -517,12 +729,12 @@ func CreateModFile(ctx context.Context, modPath string) {
        }
 
        fmt.Fprintf(os.Stderr, "go: creating new go.mod: module %s\n", modPath)
-       modFile = new(modfile.File)
+       modFile := new(modfile.File)
        modFile.AddModuleStmt(modPath)
-       initTarget(modFile.Module.Mod)
-       addGoStmt(LatestGoVersion()) // Add the go directive before converted module requirements.
+       MainModules = makeMainModules([]module.Version{modFile.Module.Mod}, []string{modRoot}, []*modfile.File{modFile}, []*modFileIndex{nil})
+       addGoStmt(modFile, modFile.Module.Mod, LatestGoVersion()) // Add the go directive before converted module requirements.
 
-       convertedFrom, err := convertLegacyConfig(modPath)
+       convertedFrom, err := convertLegacyConfig(modFile, modPath)
        if convertedFrom != "" {
                fmt.Fprintf(os.Stderr, "go: copying requirements from %s\n", base.ShortPath(convertedFrom))
        }
@@ -530,7 +742,7 @@ func CreateModFile(ctx context.Context, modPath string) {
                base.Fatalf("go: %v", err)
        }
 
-       commitRequirements(ctx, modFileGoVersion(), requirementsFromModFile(ctx))
+       commitRequirements(ctx, modFileGoVersion(), requirementsFromModFiles(ctx, []*modfile.File{modFile}))
 
        // Suggest running 'go mod tidy' unless the project is empty. Even if we
        // imported all the correct requirements above, we're probably missing
@@ -556,6 +768,24 @@ func CreateModFile(ctx context.Context, modPath string) {
        }
 }
 
+// CreateWorkFile initializes a new workspace by creating a go.work file.
+func CreateWorkFile(ctx context.Context, workFile string, modDirs []string) {
+       _ = TODOWorkspaces("Report an error if the file already exists.")
+
+       goV := LatestGoVersion() // Use current Go version by default
+       workF := new(modfile.WorkFile)
+       workF.Syntax = new(modfile.FileSyntax)
+       workF.AddGoStmt(goV)
+
+       for _, dir := range modDirs {
+               _ = TODOWorkspaces("Add the module path of the module.")
+               workF.AddDirectory(dir, "")
+       }
+
+       data := modfile.Format(workF.Syntax)
+       lockedfile.Write(workFile, bytes.NewReader(data), 0644)
+}
+
 // fixVersion returns a modfile.VersionFixer implemented using the Query function.
 //
 // It resolves commit hashes and branch names to versions,
@@ -618,47 +848,79 @@ func AllowMissingModuleImports() {
        allowMissingModuleImports = true
 }
 
-// initTarget sets Target and associated variables according to modFile,
-func initTarget(m module.Version) {
-       Target = m
-       targetPrefix = m.Path
-
-       if rel := search.InDir(base.Cwd(), cfg.GOROOTsrc); rel != "" {
-               targetInGorootSrc = true
-               if m.Path == "std" {
-                       // The "std" module in GOROOT/src is the Go standard library. Unlike other
-                       // modules, the packages in the "std" module have no import-path prefix.
-                       //
-                       // Modules named "std" outside of GOROOT/src do not receive this special
-                       // treatment, so it is possible to run 'go test .' in other GOROOTs to
-                       // test individual packages using a combination of the modified package
-                       // and the ordinary standard library.
-                       // (See https://golang.org/issue/30756.)
-                       targetPrefix = ""
+// 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) *MainModuleSet {
+       for _, m := range ms {
+               if m.Version != "" {
+                       panic("mainModulesCalled with module.Version with non empty Version field: " + fmt.Sprintf("%#v", m))
                }
        }
+       mainModules := &MainModuleSet{
+               versions:    ms[:len(ms):len(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{},
+       }
+       for i, m := range ms {
+               mainModules.pathPrefix[m] = m.Path
+               mainModules.modRoot[m] = rootDirs[i]
+               mainModules.modFiles[m] = modFiles[i]
+               mainModules.indices[m] = indices[i]
+
+               if rel := search.InDir(rootDirs[i], cfg.GOROOTsrc); rel != "" {
+                       mainModules.inGorootSrc[m] = true
+                       if m.Path == "std" {
+                               // The "std" module in GOROOT/src is the Go standard library. Unlike other
+                               // modules, the packages in the "std" module have no import-path prefix.
+                               //
+                               // Modules named "std" outside of GOROOT/src do not receive this special
+                               // treatment, so it is possible to run 'go test .' in other GOROOTs to
+                               // test individual packages using a combination of the modified package
+                               // and the ordinary standard library.
+                               // (See https://golang.org/issue/30756.)
+                               mainModules.pathPrefix[m] = ""
+                       }
+               }
+       }
+       return mainModules
 }
 
-// requirementsFromModFile returns the set of non-excluded requirements from
+// requirementsFromModFiles returns the set of non-excluded requirements from
 // the global modFile.
-func requirementsFromModFile(ctx context.Context) *Requirements {
-       roots := make([]module.Version, 0, len(modFile.Require))
-       mPathCount := map[string]int{Target.Path: 1}
+func requirementsFromModFiles(ctx context.Context, modFiles []*modfile.File) *Requirements {
+       rootCap := 0
+       for i := range modFiles {
+               rootCap += len(modFiles[i].Require)
+       }
+       roots := make([]module.Version, 0, rootCap)
+       mPathCount := make(map[string]int)
+       for _, m := range MainModules.Versions() {
+               mPathCount[m.Path] = 1
+       }
        direct := map[string]bool{}
-       for _, r := range modFile.Require {
-               if index != nil && index.exclude[r.Mod] {
-                       if cfg.BuildMod == "mod" {
-                               fmt.Fprintf(os.Stderr, "go: dropping requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
-                       } else {
-                               fmt.Fprintf(os.Stderr, "go: ignoring requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
+       for _, modFile := range modFiles {
+       requirement:
+               for _, r := range modFile.Require {
+                       // TODO(#45713): Maybe join
+                       for _, mainModule := range MainModules.Versions() {
+                               if index := MainModules.Index(mainModule); index != nil && index.exclude[r.Mod] {
+                                       if cfg.BuildMod == "mod" {
+                                               fmt.Fprintf(os.Stderr, "go: dropping requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
+                                       } else {
+                                               fmt.Fprintf(os.Stderr, "go: ignoring requirement on excluded version %s %s\n", r.Mod.Path, r.Mod.Version)
+                                       }
+                                       continue requirement
+                               }
                        }
-                       continue
-               }
 
-               roots = append(roots, r.Mod)
-               mPathCount[r.Mod.Path]++
-               if !r.Indirect {
-                       direct[r.Mod.Path] = true
+                       roots = append(roots, r.Mod)
+                       mPathCount[r.Mod.Path]++
+                       if !r.Indirect {
+                               direct[r.Mod.Path] = true
+                       }
                }
        }
        module.Sort(roots)
@@ -685,18 +947,36 @@ func requirementsFromModFile(ctx context.Context) *Requirements {
 // wasn't provided. setDefaultBuildMod may be called multiple times.
 func setDefaultBuildMod() {
        if cfg.BuildModExplicit {
+               if inWorkspaceMode() {
+                       base.Fatalf("go: -mod can't be set explicitly when in workspace mode." +
+                               "\n\tRemove the -mod flag to use the default readonly value," +
+                               "\n\tor set -workfile=off to disable workspace mode.")
+               }
                // Don't override an explicit '-mod=' argument.
                return
        }
 
-       if cfg.CmdName == "get" || strings.HasPrefix(cfg.CmdName, "mod ") {
-               // 'get' and 'go mod' commands may update go.mod automatically.
-               // TODO(jayconrod): should this narrower? Should 'go mod download' or
-               // 'go mod graph' update go.mod by default?
+       // TODO(#40775): commands should pass in the module mode as an option
+       // to modload functions instead of relying on an implicit setting
+       // based on command name.
+       switch cfg.CmdName {
+       case "get", "mod download", "mod init", "mod tidy":
+               // These commands are intended to update go.mod and go.sum.
+               cfg.BuildMod = "mod"
+               return
+       case "mod graph", "mod verify", "mod why":
+               // These commands should not update go.mod or go.sum, but they should be
+               // able to fetch modules not in go.sum and should not report errors if
+               // go.mod is inconsistent. They're useful for debugging, and they need
+               // to work in buggy situations.
                cfg.BuildMod = "mod"
+               allowWriteGoMod = false
+               return
+       case "mod vendor":
+               cfg.BuildMod = "readonly"
                return
        }
-       if modRoot == "" {
+       if modRoots == nil {
                if allowMissingModuleImports {
                        cfg.BuildMod = "mod"
                } else {
@@ -705,31 +985,38 @@ func setDefaultBuildMod() {
                return
        }
 
-       if fi, err := fsys.Stat(filepath.Join(modRoot, "vendor")); err == nil && fi.IsDir() {
-               modGo := "unspecified"
-               if index != nil && index.goVersionV != "" {
-                       if semver.Compare(index.goVersionV, "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.goVersionV[1:]
+       if len(modRoots) == 1 {
+               index := MainModules.GetSingleIndexOrNil()
+               if fi, err := fsys.Stat(filepath.Join(modRoots[0], "vendor")); err == nil && fi.IsDir() {
+                       modGo := "unspecified"
+                       if index != nil && index.goVersionV != "" {
+                               if semver.Compare(index.goVersionV, "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.goVersionV[1:]
+                               }
                        }
-               }
 
-               // Since a vendor directory exists, we should record why we didn't use it.
-               // This message won't normally be shown, but it may appear with import errors.
-               cfg.BuildModReason = fmt.Sprintf("Go version in go.mod is %s, so vendor directory was not used.", modGo)
+                       // Since a vendor directory exists, we should record why we didn't use it.
+                       // This message won't normally be shown, but it may appear with import errors.
+                       cfg.BuildModReason = fmt.Sprintf("Go version in go.mod is %s, so vendor directory was not used.", modGo)
+               }
        }
 
        cfg.BuildMod = "readonly"
 }
 
+func mustHaveCompleteRequirements() bool {
+       return cfg.BuildMod != "mod" && !inWorkspaceMode()
+}
+
 // convertLegacyConfig imports module requirements from a legacy vendoring
 // configuration file, if one is present.
-func convertLegacyConfig(modPath string) (from string, err error) {
+func convertLegacyConfig(modFile *modfile.File, modPath string) (from string, err error) {
        noneSelected := func(path string) (version string) { return "none" }
        queryPackage := func(path, rev string) (module.Version, error) {
                pkgMods, modOnly, err := QueryPattern(context.Background(), path, rev, noneSelected, nil)
@@ -742,7 +1029,10 @@ func convertLegacyConfig(modPath string) (from string, err error) {
                return modOnly.Mod, nil
        }
        for _, name := range altConfigs {
-               cfg := filepath.Join(modRoot, name)
+               if len(modRoots) != 1 {
+                       panic(TODOWorkspaces("what do do here?"))
+               }
+               cfg := filepath.Join(modRoots[0], name)
                data, err := os.ReadFile(cfg)
                if err == nil {
                        convert := modconv.Converters[name]
@@ -760,14 +1050,14 @@ func convertLegacyConfig(modPath string) (from string, err error) {
 // addGoStmt adds a go directive to the go.mod file if it does not already
 // include one. The 'go' version added, if any, is the latest version supported
 // by this toolchain.
-func addGoStmt(v string) {
+func addGoStmt(modFile *modfile.File, mod module.Version, v string) {
        if modFile.Go != nil && modFile.Go.Version != "" {
                return
        }
        if err := modFile.AddGoStmt(v); err != nil {
                base.Fatalf("go: internal error: %v", err)
        }
-       rawGoVersion.Store(Target, v)
+       rawGoVersion.Store(mod, v)
 }
 
 // LatestGoVersion returns the latest version of the Go language supported by
@@ -818,7 +1108,7 @@ var altConfigs = []string{
        ".git/config",
 }
 
-func findModuleRoot(dir string) (root string) {
+func findModuleRoots(dir string) (roots []string) {
        if dir == "" {
                panic("dir not set")
        }
@@ -827,12 +1117,37 @@ func findModuleRoot(dir string) (root string) {
        // Look for enclosing go.mod.
        for {
                if fi, err := fsys.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
-                       return dir
+                       return []string{dir}
+               }
+               d := filepath.Dir(dir)
+               if d == dir {
+                       break
+               }
+               dir = d
+       }
+       return nil
+}
+
+func findWorkspaceFile(dir string) (root string) {
+       if dir == "" {
+               panic("dir not set")
+       }
+       dir = filepath.Clean(dir)
+
+       // Look for enclosing go.mod.
+       for {
+               f := filepath.Join(dir, "go.work")
+               if fi, err := fsys.Stat(f); err == nil && !fi.IsDir() {
+                       return f
                }
                d := filepath.Dir(dir)
                if d == dir {
                        break
                }
+               if d == cfg.GOROOT {
+                       _ = TODOWorkspaces("Address how go.work files interact with GOROOT")
+                       return "" // As a special case, don't cross GOROOT to find a go.work file.
+               }
                dir = d
        }
        return ""
@@ -996,10 +1311,21 @@ func commitRequirements(ctx context.Context, goVersion string, rs *Requirements)
                return
        }
 
-       if modRoot == "" {
+       if inWorkspaceMode() {
+               // go.mod files aren't updated in workspace mode, but we still want to
+               // update the go.work.sum file.
+               if err := modfetch.WriteGoSum(keepSums(ctx, loaded, rs, addBuildListZipSums), mustHaveCompleteRequirements()); err != nil {
+                       base.Fatalf("go: %v", err)
+               }
+               return
+       }
+
+       if MainModules.Len() != 1 || MainModules.ModRoot(MainModules.Versions()[0]) == "" {
                // We aren't in a module, so we don't have anywhere to write a go.mod file.
                return
        }
+       mainModule := MainModules.Versions()[0]
+       modFile := MainModules.ModFile(mainModule)
 
        var list []*modfile.Require
        for _, m := range rs.rootModules {
@@ -1018,6 +1344,7 @@ func commitRequirements(ctx context.Context, goVersion string, rs *Requirements)
        }
        modFile.Cleanup()
 
+       index := MainModules.GetSingleIndexOrNil()
        dirty := index.modFileIsDirty(modFile)
        if dirty && cfg.BuildMod != "mod" {
                // If we're about to fail due to -mod=readonly,
@@ -1031,7 +1358,9 @@ func commitRequirements(ctx context.Context, goVersion string, rs *Requirements)
                // Don't write go.mod, but write go.sum in case we added or trimmed sums.
                // 'go mod init' shouldn't write go.sum, since it will be incomplete.
                if cfg.CmdName != "mod init" {
-                       modfetch.WriteGoSum(keepSums(ctx, loaded, rs, addBuildListZipSums))
+                       if err := modfetch.WriteGoSum(keepSums(ctx, loaded, rs, addBuildListZipSums), mustHaveCompleteRequirements()); err != nil {
+                               base.Fatalf("go: %v", err)
+                       }
                }
                return
        }
@@ -1048,13 +1377,21 @@ func commitRequirements(ctx context.Context, goVersion string, rs *Requirements)
                base.Fatalf("go: %v", err)
        }
        defer func() {
+               if MainModules.Len() != 1 {
+                       panic(TODOWorkspaces("There should be exactly one main module when committing reqs"))
+               }
+
+               mainModule := MainModules.Versions()[0]
+
                // At this point we have determined to make the go.mod file on disk equal to new.
-               index = indexModFile(new, modFile, false)
+               MainModules.SetIndex(mainModule, indexModFile(new, modFile, mainModule, false))
 
                // Update go.sum after releasing the side lock and refreshing the index.
                // 'go mod init' shouldn't write go.sum, since it will be incomplete.
                if cfg.CmdName != "mod init" {
-                       modfetch.WriteGoSum(keepSums(ctx, loaded, rs, addBuildListZipSums))
+                       if err := modfetch.WriteGoSum(keepSums(ctx, loaded, rs, addBuildListZipSums), mustHaveCompleteRequirements()); err != nil {
+                               base.Fatalf("go: %v", err)
+                       }
                }
        }()