]> Cypherpunks repositories - gostls13.git/commitdiff
[dev.cmdgo] cmd/go: add the workspace mode
authorMichael Matloob <matloob@golang.org>
Tue, 8 Jun 2021 21:07:10 +0000 (17:07 -0400)
committerMichael Matloob <matloob@golang.org>
Mon, 26 Jul 2021 21:12:11 +0000 (21:12 +0000)
This change adds the outline of the implementation of the workspace mode.
The go command will now locate go.work files, and read them to determine
which modules are in the workspace. It will then put those modules in
the root of the workspace when building the build list. It supports
building, running, testing, and listing in workspaces. There are still
many TODOs for undone work and other changes to fix certain cases. Some
of these undone parts include: replaces and go.work.sum files, as well
as go mod {test,why,verify}, excludes in workspaces, updating work files
to include module names in comments and setting the GOWORK variable.

For #45713

Change-Id: I72716af7a300a2896087fc8a79c04e951d248278
Reviewed-on: https://go-review.googlesource.com/c/go/+/334934
Trust: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
15 files changed:
src/cmd/go/alldocs.go
src/cmd/go/internal/base/flag.go
src/cmd/go/internal/cfg/cfg.go
src/cmd/go/internal/envcmd/env.go
src/cmd/go/internal/list/list.go
src/cmd/go/internal/modcmd/download.go
src/cmd/go/internal/modcmd/graph.go
src/cmd/go/internal/modcmd/verify.go
src/cmd/go/internal/modcmd/why.go
src/cmd/go/internal/modload/init.go
src/cmd/go/internal/run/run.go
src/cmd/go/internal/test/test.go
src/cmd/go/internal/test/testflag.go
src/cmd/go/internal/work/build.go
src/cmd/go/testdata/script/work.txt [new file with mode: 0644]

index 90eb3e2a00b56ddc5e878672f7f18f42b9d2cbf4..e7c2e6b51bb4a141f15dd2443bf9e99bb5304f58 100644 (file)
 //             directory, but it is not accessed. When -modfile is specified, an
 //             alternate go.sum file is also used: its path is derived from the
 //             -modfile flag by trimming the ".mod" extension and appending ".sum".
+//   -workfile file
+//     in module aware mode, use the given go.work file as a workspace file.
+//             By default or when -workfile is "auto", the go command searches for a
+//             file named go.work in the current directory and then containing directories
+//             until one is found. If a valid go.work file is found, the modules
+//             specified will collectively be used as the main modules. If -workfile
+//             is "off", or a go.work file is not found in "auto" mode, workspace
+//             mode is disabled.
 //     -overlay file
 //             read a JSON config file that provides an overlay for build operations.
 //             The file is a JSON struct with a single field, named 'Replace', that
index 677f8196827f4c8fa18e49964ba2abc75a5861fa..2262e2e992bccd469d63b1f43e95167732b00a95 100644 (file)
@@ -62,6 +62,13 @@ func AddModFlag(flags *flag.FlagSet) {
        flags.Var(explicitStringFlag{value: &cfg.BuildMod, explicit: &cfg.BuildModExplicit}, "mod", "")
 }
 
+// AddWorkfileFlag adds the workfile flag to the flag set. It enables workspace
+// mode for commands that support it by resetting the cfg.WorkFile variable
+// to "" (equivalent to auto) rather than off.
+func AddWorkfileFlag(flags *flag.FlagSet) {
+       flags.Var(explicitStringFlag{value: &cfg.WorkFile, explicit: &cfg.WorkFileExplicit}, "workfile", "")
+}
+
 // AddModCommonFlags adds the module-related flags common to build commands
 // and 'go mod' subcommands.
 func AddModCommonFlags(flags *flag.FlagSet) {
index 57a3c1ff6fbdc1f9f29a10a4580e8d40e4f8c908..da616ee1dd5c9d7c75b921eadf6ff7652598b4d3 100644 (file)
@@ -47,8 +47,10 @@ var (
        BuildWork              bool // -work flag
        BuildX                 bool // -x flag
 
-       ModCacheRW bool   // -modcacherw flag
-       ModFile    string // -modfile flag
+       ModCacheRW       bool   // -modcacherw flag
+       ModFile          string // -modfile flag
+       WorkFile         string // -workfile flag
+       WorkFileExplicit bool   // whether -workfile was set explicitly
 
        CmdName string // "build", "install", "list", "mod tidy", etc.
 
index 1553d263914541f05a639e03b8041969ba244301..f68090f21f36bd7bbc0ea43fd94f3736d6760ddc 100644 (file)
@@ -145,6 +145,7 @@ func findEnv(env []cfg.EnvVar, name string) string {
 // ExtraEnvVars returns environment variables that should not leak into child processes.
 func ExtraEnvVars() []cfg.EnvVar {
        gomod := ""
+       modload.Init()
        if modload.HasModRoot() {
                gomod = filepath.Join(modload.ModRoot(), "go.mod")
        } else if modload.Enabled() {
index 7cb9ec6d9492428aa0c9afe111fde89bf35fa536..04630dc341e803c507cab089c61da7938d0ca6d2 100644 (file)
@@ -316,6 +316,7 @@ For more about modules, see https://golang.org/ref/mod.
 func init() {
        CmdList.Run = runList // break init cycle
        work.AddBuildFlags(CmdList, work.DefaultBuildFlags)
+       base.AddWorkfileFlag(&CmdList.Flag)
 }
 
 var (
@@ -336,6 +337,8 @@ var (
 var nl = []byte{'\n'}
 
 func runList(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
+
        if *listFmt != "" && *listJson == true {
                base.Fatalf("go list -f cannot be used with -json")
        }
index 3c88a4b900ebb0aee679bea5b472fe30f0263c45..6a99cb01e1eeac616083ace0977d20174f8285ba 100644 (file)
@@ -66,6 +66,7 @@ func init() {
        // TODO(jayconrod): https://golang.org/issue/35849 Apply -x to other 'go mod' commands.
        cmdDownload.Flag.BoolVar(&cfg.BuildX, "x", false, "")
        base.AddModCommonFlags(&cmdDownload.Flag)
+       base.AddWorkfileFlag(&cmdDownload.Flag)
 }
 
 type moduleJSON struct {
@@ -81,6 +82,8 @@ type moduleJSON struct {
 }
 
 func runDownload(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
+
        // Check whether modules are enabled and whether we're in a module.
        modload.ForceUseModules = true
        if !modload.HasModRoot() && len(args) == 0 {
index 903bd9970f1a5b3fa9eed70678b7f0ee4219fabc..5ef1d4ed04aff9eb9df3745e4b6bed12e87fe709 100644 (file)
@@ -42,9 +42,12 @@ var (
 func init() {
        cmdGraph.Flag.Var(&graphGo, "go", "")
        base.AddModCommonFlags(&cmdGraph.Flag)
+       base.AddWorkfileFlag(&cmdGraph.Flag)
 }
 
 func runGraph(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
+
        if len(args) > 0 {
                base.Fatalf("go mod graph: graph takes no arguments")
        }
index 5a6eca32cfb706bfcd50906937d9eec70e4fc4c4..14c4d76bc364061821f7a185cc9b1930413f6c87 100644 (file)
@@ -39,9 +39,12 @@ See https://golang.org/ref/mod#go-mod-verify for more about 'go mod verify'.
 
 func init() {
        base.AddModCommonFlags(&cmdVerify.Flag)
+       base.AddWorkfileFlag(&cmdVerify.Flag)
 }
 
 func runVerify(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
+
        if len(args) != 0 {
                // NOTE(rsc): Could take a module pattern.
                base.Fatalf("go mod verify: verify takes no arguments")
index 3b14b27c8c780d954a133cd46dc4bee2f875e442..eef5fa5ae87cdeb0849645f6b52a0c4c86635488 100644 (file)
@@ -61,9 +61,11 @@ var (
 func init() {
        cmdWhy.Run = runWhy // break init cycle
        base.AddModCommonFlags(&cmdWhy.Flag)
+       base.AddWorkfileFlag(&cmdWhy.Flag)
 }
 
 func runWhy(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
        modload.ForceUseModules = true
        modload.RootMode = modload.NeedRoot
 
index 33f916303858cc02753c209d92204e39b3cca575..f211e1767c21089e8edefe42ba970370907d6d9a 100644 (file)
@@ -53,10 +53,6 @@ func TODOWorkspaces(s string) error {
 var (
        initialized bool
 
-       // The directory containing go.work file. Set if in a go.work file is found
-       // and the go command is operating in workspace mode.
-       workRoot 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
@@ -66,6 +62,12 @@ var (
        gopath   string
 )
 
+// Variable set in InitWorkfile
+var (
+       // 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
@@ -187,6 +189,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
@@ -259,6 +275,8 @@ func Init() {
                        base.Fatalf("go: -modfile cannot be used with commands that ignore the current module")
                }
                modRoots = nil
+       } else if inWorkspaceMode() {
+               // We're in workspace mode.
        } else {
                modRoots = findModuleRoots(base.Cwd())
                if modRoots == nil {
@@ -293,6 +311,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")
@@ -302,7 +321,17 @@ func Init() {
                base.Fatalf("$GOPATH/go.mod exists but should not")
        }
 
-       if modRoots == nil {
+       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)
+               }
+               // 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
@@ -388,12 +417,24 @@ func ModRoot() string {
        if !HasModRoot() {
                die()
        }
+       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(TODOWorkspaces("need to handle multiple modroots here"))
+               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.
 // HasModRoot may return false even if Enabled returns true: for example, 'get'
 // does not require a main module.
@@ -451,6 +492,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.
 //
@@ -498,40 +564,62 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
                return requirements, false
        }
 
-       gomod := ModFilePath()
-       data, err := lockedfile.Read(gomod)
-       if err != nil {
-               base.Fatalf("go: %v", err)
-       }
+       var modFiles []*modfile.File
+       var mainModules []module.Version
+       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")
+               }
 
-       // For now, this code assumes there's a single main module, because there's
-       // no way to specify multiple main modules yet. TODO(#45713): update this
-       // in a later CL.
-       modFile = f
-       mainModule := f.Module.Mod
-       MainModules = makeMainModules([]module.Version{mainModule}, modRoots)
-       index = indexModFile(data, f, mainModule, fixed)
+               modFile = f // TODO(golang.org/cl/327329): remove the global modFile variable and replace it with multiple modfiles
+               modFiles = append(modFiles, f)
+               mainModule := f.Module.Mod
+               mainModules = append(mainModules, mainModule)
+               index = 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)
        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()
@@ -549,6 +637,7 @@ func loadModFile(ctx context.Context) (rs *Requirements, needCommit bool) {
                                // 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)
@@ -613,7 +702,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
@@ -737,29 +826,36 @@ func makeMainModules(ms []module.Version, rootDirs []string) *MainModuleSet {
        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))
+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 {
+               // TODO(golang.org/cl/327329): Use the correct index here.
+               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)
+                               }
+                               continue
                        }
-                       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)
@@ -786,6 +882,11 @@ 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
        }
@@ -944,6 +1045,31 @@ func findModuleRoots(dir string) (roots []string) {
        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 ""
+}
+
 func findAltConfig(dir string) (root, name string) {
        if dir == "" {
                panic("dir not set")
index 784f7162dfd3f5b01bbd7c60a89975a9e3e5f1aa..7d9e2930aba2bd8b99d9e77787984f176f3337c1 100644 (file)
@@ -65,6 +65,7 @@ func init() {
        CmdRun.Run = runRun // break init loop
 
        work.AddBuildFlags(CmdRun, work.DefaultBuildFlags)
+       base.AddWorkfileFlag(&CmdRun.Flag)
        CmdRun.Flag.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
 }
 
@@ -73,6 +74,8 @@ func printStderr(args ...interface{}) (int, error) {
 }
 
 func runRun(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
+
        if shouldUseOutsideModuleMode(args) {
                // Set global module flags for 'go run cmd@version'.
                // This must be done before modload.Init, but we need to call work.BuildInit
index 59ea1ef5445178f052006dd37af2d8b0b209b9ef..5fcea18caa9e76bca0591982ae5806d6b8492a7e 100644 (file)
@@ -29,6 +29,7 @@ import (
        "cmd/go/internal/cfg"
        "cmd/go/internal/load"
        "cmd/go/internal/lockedfile"
+       "cmd/go/internal/modload"
        "cmd/go/internal/search"
        "cmd/go/internal/str"
        "cmd/go/internal/trace"
@@ -577,6 +578,7 @@ var defaultVetFlags = []string{
 }
 
 func runTest(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
        pkgArgs, testArgs = testFlags(args)
 
        if cfg.DebugTrace != "" {
index 08f1efa2c0d26a0cf398f2b778c0adde90fc97ae..f129346d0dac3cc47fdb02b97d3d4d047ceea5ff 100644 (file)
@@ -29,6 +29,7 @@ import (
 
 func init() {
        work.AddBuildFlags(CmdTest, work.OmitVFlag)
+       base.AddWorkfileFlag(&CmdTest.Flag)
 
        cf := CmdTest.Flag
        cf.BoolVar(&testC, "c", false, "")
index 0ed2389cd5a81af6f3ff862dd275b9fb9ba0109e..c51dd398c241f79a42ee9aaca8e246686a86e4d1 100644 (file)
@@ -121,6 +121,14 @@ and test commands:
                directory, but it is not accessed. When -modfile is specified, an
                alternate go.sum file is also used: its path is derived from the
                -modfile flag by trimming the ".mod" extension and appending ".sum".
+  -workfile file
+    in module aware mode, use the given go.work file as a workspace file.
+               By default or when -workfile is "auto", the go command searches for a
+               file named go.work in the current directory and then containing directories
+               until one is found. If a valid go.work file is found, the modules
+               specified will collectively be used as the main modules. If -workfile
+               is "off", or a go.work file is not found in "auto" mode, workspace
+               mode is disabled.
        -overlay file
                read a JSON config file that provides an overlay for build operations.
                The file is a JSON struct with a single field, named 'Replace', that
@@ -201,6 +209,7 @@ func init() {
 
        AddBuildFlags(CmdBuild, DefaultBuildFlags)
        AddBuildFlags(CmdInstall, DefaultBuildFlags)
+       base.AddWorkfileFlag(&CmdBuild.Flag)
 }
 
 // Note that flags consulted by other parts of the code
@@ -364,6 +373,7 @@ var pkgsFilter = func(pkgs []*load.Package) []*load.Package { return pkgs }
 var runtimeVersion = runtime.Version()
 
 func runBuild(ctx context.Context, cmd *base.Command, args []string) {
+       modload.InitWorkfile()
        BuildInit()
        var b Builder
        b.Init()
diff --git a/src/cmd/go/testdata/script/work.txt b/src/cmd/go/testdata/script/work.txt
new file mode 100644 (file)
index 0000000..f2b51ca
--- /dev/null
@@ -0,0 +1,71 @@
+go run example.com/b
+stdout 'Hello from module A'
+
+# And try from a different directory
+cd c
+go run example.com/b
+stdout 'Hello from module A'
+cd $GOPATH/src
+
+go list all # all includes both modules
+stdout 'example.com/a'
+stdout 'example.com/b'
+
+# -mod can't be set in workspace mode, even to readonly
+! go list -mod=readonly all
+stderr '^go: -mod can''t be set explicitly'
+go list -mod=readonly -workfile=off all
+
+# Test that duplicates in the directory list return an error
+cp go.work go.work.backup
+cp go.work.dup go.work
+! go run example.com/b
+stderr 'reading go.work: path .* appears multiple times in workspace'
+cp go.work.backup go.work
+
+-- go.work.dup --
+go 1.17
+
+directory (
+  a
+  b
+  ../src/a
+)
+-- go.work --
+go 1.17
+
+directory (
+  ./a
+  ./b
+)
+
+-- a/go.mod --
+
+module example.com/a
+
+-- a/a.go --
+package a
+
+import "fmt"
+
+func HelloFromA() {
+  fmt.Println("Hello from module A")
+}
+
+-- b/go.mod --
+
+module example.com/b
+
+-- b/main.go --
+package main
+
+import "example.com/a"
+
+func main() {
+  a.HelloFromA()
+}
+
+-- c/README --
+Create this directory so we can cd to
+it and make sure paths are interpreted
+relative to the go.work, not the cwd.