cmd/go: implement 'go install pkg@version'

With this change, 'go install' will install executables in module mode
without using or modifying the module in the current directory, if
there is one.

For #40276

Change-Id: I922e71719b3a4e0c779ce7a30429355fc29930bf
Reviewed-on: https://go-review.googlesource.com/c/go/+/254365
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/doc/go1.16.html b/doc/go1.16.html
index 95e63d0..f177226 100644
--- a/doc/go1.16.html
+++ b/doc/go1.16.html
@@ -43,6 +43,18 @@
 
 <h3 id="go-command">Go command</h3>
 
+<p><!-- golang.org/issue/40276 -->
+  <code>go</code> <code>install</code> now accepts arguments with
+  version suffixes (for example, <code>go</code> <code>install</code>
+  <code>example.com/cmd@v1.0.0</code>). This causes <code>go</code>
+  <code>install</code> to build and install packages in module-aware mode,
+  ignoring the <code>go.mod</code> file in the current directory or any parent
+  directory, if there is one. This is useful for installing executables without
+  affecting the dependencies of the main module.<br>
+  TODO: write and link to section in golang.org/ref/mod<br>
+  TODO: write and link to blog post
+</p>
+
 <p><!-- golang.org/issue/24031 -->
   <code>retract</code> directives may now be used in a <code>go.mod</code> file
   to indicate that certain published versions of the module should not be used
diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go
index 8ad4f66..104aea6 100644
--- a/src/cmd/go/alldocs.go
+++ b/src/cmd/go/alldocs.go
@@ -715,6 +715,33 @@
 // environment variable is not set. Executables in $GOROOT
 // are installed in $GOROOT/bin or $GOTOOLDIR instead of $GOBIN.
 //
+// If the arguments have version suffixes (like @latest or @v1.0.0), "go install"
+// builds packages in module-aware mode, ignoring the go.mod file in the current
+// directory or any parent directory, if there is one. This is useful for
+// installing executables without affecting the dependencies of the main module.
+// To eliminate ambiguity about which module versions are used in the build, the
+// arguments must satisfy the following constraints:
+//
+// - Arguments must be package paths or package patterns (with "..." wildcards).
+//   They must not be standard packages (like fmt), meta-patterns (std, cmd,
+//   all), or relative or absolute file paths.
+// - All arguments must have the same version suffix. Different queries are not
+//   allowed, even if they refer to the same version.
+// - All arguments must refer to packages in the same module at the same version.
+// - No module is considered the "main" module. If the module containing
+//   packages named on the command line has a go.mod file, it must not contain
+//   directives (replace and exclude) that would cause it to be interpreted
+//   differently than if it were the main module. The module must not require
+//   a higher version of itself.
+// - Package path arguments must refer to main packages. Pattern arguments
+//   will only match main packages.
+//
+// If the arguments don't have version suffixes, "go install" may run in
+// module-aware mode or GOPATH mode, depending on the GO111MODULE environment
+// variable and the presence of a go.mod file. See 'go help modules' for details.
+// If module-aware mode is enabled, "go install" runs in the context of the main
+// module.
+//
 // When module-aware mode is disabled, other packages are installed in the
 // directory $GOPATH/pkg/$GOOS_$GOARCH. When module-aware mode is enabled,
 // other packages are built and cached but not installed.
diff --git a/src/cmd/go/internal/modload/init.go b/src/cmd/go/internal/modload/init.go
index 1f50dcb..2f0f60b 100644
--- a/src/cmd/go/internal/modload/init.go
+++ b/src/cmd/go/internal/modload/init.go
@@ -35,8 +35,7 @@
 )
 
 var (
-	mustUseModules = false
-	initialized    bool
+	initialized bool
 
 	modRoot string
 	Target  module.Version
@@ -55,9 +54,33 @@
 	CmdModInit   bool   // running 'go mod init'
 	CmdModModule string // module argument for 'go mod init'
 
+	// RootMode determines whether a module root is needed.
+	RootMode Root
+
+	// ForceUseModules may be set to force modules to be enabled when
+	// GO111MODULE=auto or to report an error when GO111MODULE=off.
+	ForceUseModules bool
+
 	allowMissingModuleImports bool
 )
 
+type Root int
+
+const (
+	// AutoRoot is the default for most commands. modload.Init will look for
+	// a go.mod file in the current directory or any parent. If none is found,
+	// modules may be disabled (GO111MODULE=on) or commands may run in a
+	// limited module mode.
+	AutoRoot Root = iota
+
+	// NoRoot is used for commands that run in module mode and ignore any go.mod
+	// file the current directory or in parent directories.
+	NoRoot
+
+	// TODO(jayconrod): add NeedRoot for commands like 'go mod vendor' that
+	// don't make sense without a main module.
+)
+
 // ModFile returns the parsed go.mod file.
 //
 // Note that after calling ImportPaths or LoadBuildList,
@@ -92,15 +115,19 @@
 	// 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.
+	var mustUseModules bool
 	env := cfg.Getenv("GO111MODULE")
 	switch env {
 	default:
 		base.Fatalf("go: unknown environment setting GO111MODULE=%s", env)
 	case "auto", "":
-		mustUseModules = false
+		mustUseModules = ForceUseModules
 	case "on":
 		mustUseModules = true
 	case "off":
+		if ForceUseModules {
+			base.Fatalf("go: modules disabled by GO111MODULE=off; see 'go help modules'")
+		}
 		mustUseModules = false
 		return
 	}
@@ -135,6 +162,10 @@
 	if CmdModInit {
 		// Running 'go mod init': go.mod will be created in current directory.
 		modRoot = base.Cwd
+	} else if RootMode == NoRoot {
+		// TODO(jayconrod): report an error if -mod -modfile is explicitly set on
+		// the command line. Ignore those flags if they come from GOFLAGS.
+		modRoot = ""
 	} else {
 		modRoot = findModuleRoot(base.Cwd)
 		if modRoot == "" {
@@ -154,6 +185,9 @@
 			// when it happens. See golang.org/issue/26708.
 			modRoot = ""
 			fmt.Fprintf(os.Stderr, "go: warning: ignoring go.mod in system temp root %v\n", os.TempDir())
+			if !mustUseModules {
+				return
+			}
 		}
 	}
 	if cfg.ModFile != "" && !strings.HasSuffix(cfg.ModFile, ".mod") {
@@ -219,10 +253,12 @@
 // 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 {
+	if modRoot != "" || cfg.ModulesEnabled {
+		// Already enabled.
 		return true
 	}
 	if initialized {
+		// Initialized, not enabled.
 		return false
 	}
 
@@ -263,7 +299,7 @@
 // (usually through MustModRoot).
 func Enabled() bool {
 	Init()
-	return modRoot != "" || mustUseModules
+	return modRoot != "" || cfg.ModulesEnabled
 }
 
 // ModRoot returns the root of the main module.
diff --git a/src/cmd/go/internal/work/build.go b/src/cmd/go/internal/work/build.go
index e99982e..990e5d9 100644
--- a/src/cmd/go/internal/work/build.go
+++ b/src/cmd/go/internal/work/build.go
@@ -9,8 +9,10 @@
 	"errors"
 	"fmt"
 	"go/build"
+	"internal/goroot"
 	"os"
 	"os/exec"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -18,8 +20,13 @@
 	"cmd/go/internal/base"
 	"cmd/go/internal/cfg"
 	"cmd/go/internal/load"
+	"cmd/go/internal/modfetch"
+	"cmd/go/internal/modload"
 	"cmd/go/internal/search"
 	"cmd/go/internal/trace"
+
+	"golang.org/x/mod/modfile"
+	"golang.org/x/mod/module"
 )
 
 var CmdBuild = &base.Command{
@@ -440,6 +447,33 @@
 environment variable is not set. Executables in $GOROOT
 are installed in $GOROOT/bin or $GOTOOLDIR instead of $GOBIN.
 
+If the arguments have version suffixes (like @latest or @v1.0.0), "go install"
+builds packages in module-aware mode, ignoring the go.mod file in the current
+directory or any parent directory, if there is one. This is useful for
+installing executables without affecting the dependencies of the main module.
+To eliminate ambiguity about which module versions are used in the build, the
+arguments must satisfy the following constraints:
+
+- Arguments must be package paths or package patterns (with "..." wildcards).
+  They must not be standard packages (like fmt), meta-patterns (std, cmd,
+  all), or relative or absolute file paths.
+- All arguments must have the same version suffix. Different queries are not
+  allowed, even if they refer to the same version.
+- All arguments must refer to packages in the same module at the same version.
+- No module is considered the "main" module. If the module containing
+  packages named on the command line has a go.mod file, it must not contain
+  directives (replace and exclude) that would cause it to be interpreted
+  differently than if it were the main module. The module must not require
+  a higher version of itself.
+- Package path arguments must refer to main packages. Pattern arguments
+  will only match main packages.
+
+If the arguments don't have version suffixes, "go install" may run in
+module-aware mode or GOPATH mode, depending on the GO111MODULE environment
+variable and the presence of a go.mod file. See 'go help modules' for details.
+If module-aware mode is enabled, "go install" runs in the context of the main
+module.
+
 When module-aware mode is disabled, other packages are installed in the
 directory $GOPATH/pkg/$GOOS_$GOARCH. When module-aware mode is enabled,
 other packages are built and cached but not installed.
@@ -510,6 +544,12 @@
 }
 
 func runInstall(ctx context.Context, cmd *base.Command, args []string) {
+	for _, arg := range args {
+		if strings.Contains(arg, "@") && !build.IsLocalImport(arg) && !filepath.IsAbs(arg) {
+			installOutsideModule(ctx, args)
+			return
+		}
+	}
 	BuildInit()
 	InstallPackages(ctx, args, load.PackagesForBuild(ctx, args))
 }
@@ -634,6 +674,158 @@
 	}
 }
 
+// installOutsideModule implements 'go install pkg@version'. It builds and
+// installs one or more main packages in module mode while ignoring any go.mod
+// in the current directory or parent directories.
+//
+// See golang.org/issue/40276 for details and rationale.
+func installOutsideModule(ctx context.Context, args []string) {
+	modload.ForceUseModules = true
+	modload.RootMode = modload.NoRoot
+	modload.AllowMissingModuleImports()
+	modload.Init()
+
+	// Check that the arguments satisfy syntactic constraints.
+	var version string
+	for _, arg := range args {
+		if i := strings.Index(arg, "@"); i >= 0 {
+			version = arg[i+1:]
+			if version == "" {
+				base.Fatalf("go install %s: version must not be empty", arg)
+			}
+			break
+		}
+	}
+	patterns := make([]string, len(args))
+	for i, arg := range args {
+		if !strings.HasSuffix(arg, "@"+version) {
+			base.Errorf("go install %s: all arguments must have the same version (@%s)", arg, version)
+			continue
+		}
+		p := arg[:len(arg)-len(version)-1]
+		switch {
+		case build.IsLocalImport(p):
+			base.Errorf("go install %s: argument must be a package path, not a relative path", arg)
+		case filepath.IsAbs(p):
+			base.Errorf("go install %s: argument must be a package path, not an absolute path", arg)
+		case search.IsMetaPackage(p):
+			base.Errorf("go install %s: argument must be a package path, not a meta-package", arg)
+		case path.Clean(p) != p:
+			base.Errorf("go install %s: argument must be a clean package path", arg)
+		case !strings.Contains(p, "...") && search.IsStandardImportPath(p) && goroot.IsStandardPackage(cfg.GOROOT, cfg.BuildContext.Compiler, p):
+			base.Errorf("go install %s: argument must not be a package in the standard library", arg)
+		default:
+			patterns[i] = p
+		}
+	}
+	base.ExitIfErrors()
+	BuildInit()
+
+	// Query the module providing the first argument, load its go.mod file, and
+	// check that it doesn't contain directives that would cause it to be
+	// interpreted differently if it were the main module.
+	//
+	// If multiple modules match the first argument, accept the longest match
+	// (first result). It's possible this module won't provide packages named by
+	// later arguments, and other modules would. Let's not try to be too
+	// magical though.
+	allowed := modload.CheckAllowed
+	if modload.IsRevisionQuery(version) {
+		// Don't check for retractions if a specific revision is requested.
+		allowed = nil
+	}
+	qrs, err := modload.QueryPattern(ctx, patterns[0], version, allowed)
+	if err != nil {
+		base.Fatalf("go install %s: %v", args[0], err)
+	}
+	installMod := qrs[0].Mod
+	data, err := modfetch.GoMod(installMod.Path, installMod.Version)
+	if err != nil {
+		base.Fatalf("go install %s: %v", args[0], err)
+	}
+	f, err := modfile.Parse("go.mod", data, nil)
+	if err != nil {
+		base.Fatalf("go install %s: %s: %v", args[0], installMod, err)
+	}
+	directiveFmt := "go install %s: %s\n" +
+		"\tThe go.mod file for the module providing named packages contains one or\n" +
+		"\tmore %s directives. It must not contain directives that would cause\n" +
+		"\tit to be interpreted differently than if it were the main module."
+	if len(f.Replace) > 0 {
+		base.Fatalf(directiveFmt, args[0], installMod, "replace")
+	}
+	if len(f.Exclude) > 0 {
+		base.Fatalf(directiveFmt, args[0], installMod, "exclude")
+	}
+
+	// Initialize the build list using a dummy main module that requires the
+	// module providing the packages on the command line.
+	target := module.Version{Path: "go-install-target"}
+	modload.SetBuildList([]module.Version{target, installMod})
+
+	// Load packages for all arguments. Ignore non-main packages.
+	// Print a warning if an argument contains "..." and matches no main packages.
+	// PackagesForBuild already prints warnings for patterns that don't match any
+	// packages, so be careful not to double print.
+	matchers := make([]func(string) bool, len(patterns))
+	for i, p := range patterns {
+		if strings.Contains(p, "...") {
+			matchers[i] = search.MatchPattern(p)
+		}
+	}
+
+	// TODO(golang.org/issue/40276): don't report errors loading non-main packages
+	// matched by a pattern.
+	pkgs := load.PackagesForBuild(ctx, patterns)
+	mainPkgs := make([]*load.Package, 0, len(pkgs))
+	mainCount := make([]int, len(patterns))
+	nonMainCount := make([]int, len(patterns))
+	for _, pkg := range pkgs {
+		if pkg.Name == "main" {
+			mainPkgs = append(mainPkgs, pkg)
+			for i := range patterns {
+				if matchers[i] != nil && matchers[i](pkg.ImportPath) {
+					mainCount[i]++
+				}
+			}
+		} else {
+			for i := range patterns {
+				if matchers[i] == nil && patterns[i] == pkg.ImportPath {
+					base.Errorf("go install: package %s is not a main package", pkg.ImportPath)
+				} else if matchers[i] != nil && matchers[i](pkg.ImportPath) {
+					nonMainCount[i]++
+				}
+			}
+		}
+	}
+	base.ExitIfErrors()
+	for i, p := range patterns {
+		if matchers[i] != nil && mainCount[i] == 0 && nonMainCount[i] > 0 {
+			fmt.Fprintf(os.Stderr, "go: warning: %q matched no main packages\n", p)
+		}
+	}
+
+	// Check that named packages are all provided by the same module.
+	for _, mod := range modload.LoadedModules() {
+		if mod.Path == installMod.Path && mod.Version != installMod.Version {
+			base.Fatalf("go install: %s: module requires a higher version of itself (%s)", installMod, mod.Version)
+		}
+	}
+	for _, pkg := range mainPkgs {
+		if pkg.Module == nil {
+			// Packages in std, cmd, and their vendored dependencies
+			// don't have this field set.
+			base.Errorf("go install: package %s not provided by module %s", pkg.ImportPath, installMod)
+		} else if pkg.Module.Path != installMod.Path || pkg.Module.Version != installMod.Version {
+			base.Errorf("go install: package %s provided by module %s@%s\n\tAll packages must be provided by the same module (%s).", pkg.ImportPath, pkg.Module.Path, pkg.Module.Version, installMod)
+		}
+	}
+	base.ExitIfErrors()
+
+	// Build and install the packages.
+	InstallPackages(ctx, patterns, mainPkgs)
+}
+
 // ExecCmd is the command to use to run user binaries.
 // Normally it is empty, meaning run the binaries directly.
 // If cross-compiling and running on a remote system or
diff --git a/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-exclude.txt b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-exclude.txt
new file mode 100644
index 0000000..c883d8a
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-exclude.txt
@@ -0,0 +1,28 @@
+example.com/cmd contains main packages.
+
+-- .info --
+{"Version":"v1.0.0-exclude"}
+-- .mod --
+module example.com/cmd
+
+go 1.16
+
+exclude rsc.io/quote v1.5.2
+-- go.mod --
+module example.com/cmd
+
+go 1.16
+
+exclude rsc.io/quote v1.5.2
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
+-- err/err.go --
+package err
+
+var X = DoesNotCompile
diff --git a/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-newerself.txt b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-newerself.txt
new file mode 100644
index 0000000..7670f29
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-newerself.txt
@@ -0,0 +1,28 @@
+example.com/cmd contains main packages.
+
+-- .info --
+{"Version":"v1.0.0-newerself"}
+-- .mod --
+module example.com/cmd
+
+go 1.16
+
+require example.com/cmd v1.0.0
+-- go.mod --
+module example.com/cmd
+
+go 1.16
+
+require example.com/cmd v1.0.0
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
+-- err/err.go --
+package err
+
+var X = DoesNotCompile
diff --git a/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-replace.txt b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-replace.txt
new file mode 100644
index 0000000..581a496
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0-replace.txt
@@ -0,0 +1,28 @@
+example.com/cmd contains main packages.
+
+-- .info --
+{"Version":"v1.0.0-replace"}
+-- .mod --
+module example.com/cmd
+
+go 1.16
+
+replace rsc.io/quote => rsc.io/quote v1.5.2
+-- go.mod --
+module example.com/cmd
+
+go 1.16
+
+replace rsc.io/quote => rsc.io/quote v1.5.2
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
+-- err/err.go --
+package err
+
+var X = DoesNotCompile
diff --git a/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0.txt b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0.txt
new file mode 100644
index 0000000..ee43938
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_cmd_v1.0.0.txt
@@ -0,0 +1,27 @@
+example.com/cmd contains main packages.
+
+v1.0.0 is the latest non-retracted version. Other versions contain errors or
+detectable problems.
+
+-- .info --
+{"Version":"v1.0.0"}
+-- .mod --
+module example.com/cmd
+
+go 1.16
+-- go.mod --
+module example.com/cmd
+
+go 1.16
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
+-- err/err.go --
+package err
+
+var X = DoesNotCompile
diff --git a/src/cmd/go/testdata/mod/example.com_cmd_v1.9.0.txt b/src/cmd/go/testdata/mod/example.com_cmd_v1.9.0.txt
new file mode 100644
index 0000000..9298afb
--- /dev/null
+++ b/src/cmd/go/testdata/mod/example.com_cmd_v1.9.0.txt
@@ -0,0 +1,30 @@
+example.com/cmd contains main packages.
+
+-- .info --
+{"Version":"v1.9.0"}
+-- .mod --
+module example.com/cmd
+
+go 1.16
+
+// this is a bad version
+retract v1.9.0
+-- go.mod --
+module example.com/cmd
+
+go 1.16
+
+// this is a bad version
+retract v1.9.0
+-- a/a.go --
+package main
+
+func main() {}
+-- b/b.go --
+package main
+
+func main() {}
+-- err/err.go --
+package err
+
+var X = DoesNotCompile
diff --git a/src/cmd/go/testdata/script/mod_install_pkg_version.txt b/src/cmd/go/testdata/script/mod_install_pkg_version.txt
new file mode 100644
index 0000000..7e6d4e8
--- /dev/null
+++ b/src/cmd/go/testdata/script/mod_install_pkg_version.txt
@@ -0,0 +1,187 @@
+# 'go install pkg@version' works outside a module.
+env GO111MODULE=auto
+go install example.com/cmd/a@v1.0.0
+exists $GOPATH/bin/a$GOEXE
+rm $GOPATH/bin
+
+
+# 'go install pkg@version' reports an error if modules are disabled.
+env GO111MODULE=off
+! go install example.com/cmd/a@v1.0.0
+stderr '^go: modules disabled by GO111MODULE=off; see ''go help modules''$'
+env GO111MODULE=auto
+
+
+# 'go install pkg@version' ignores go.mod in current directory.
+cd m
+cp go.mod go.mod.orig
+! go list -m all
+stderr 'example.com/cmd@v1.1.0-doesnotexist:.*404 Not Found'
+go install example.com/cmd/a@latest
+cmp go.mod go.mod.orig
+exists $GOPATH/bin/a$GOEXE
+go version -m $GOPATH/bin/a$GOEXE
+stdout '^\tmod\texample.com/cmd\tv1.0.0\t' # "latest", not from go.mod
+rm $GOPATH/bin/a
+cd ..
+
+
+# Every test case requires linking, so we only cover the most important cases
+# when -short is set.
+[short] stop
+
+
+# 'go install pkg@version' works on a module that doesn't have a go.mod file
+# and with a module whose go.mod file has missing requirements.
+# With a proxy, the two cases are indistinguishable.
+go install rsc.io/fortune@v1.0.0
+stderr '^go: found rsc.io/quote in rsc.io/quote v1.5.2$'
+exists $GOPATH/bin/fortune$GOEXE
+! exists $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0/go.mod # no go.mod file
+go version -m $GOPATH/bin/fortune$GOEXE
+stdout '^\tdep\trsc.io/quote\tv1.5.2\t' # latest version of fortune's dependency
+rm $GOPATH/bin
+
+
+# 'go install dir@version' works like a normal 'go install' command if
+# dir is a relative or absolute path.
+env GO111MODULE=on
+go mod download rsc.io/fortune@v1.0.0
+! go install $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
+stderr '^go: cannot find main module; see ''go help modules''$'
+! go install ../pkg/mod/rsc.io/fortune@v1.0.0
+stderr '^go: cannot find main module; see ''go help modules''$'
+mkdir tmp
+cd tmp
+go mod init tmp
+go mod edit -require=rsc.io/fortune@v1.0.0
+! go install -mod=readonly $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
+stderr '^go: updates to go.sum needed, disabled by -mod=readonly$'
+! go install -mod=readonly ../../pkg/mod/rsc.io/fortune@v1.0.0
+stderr '^go: updates to go.sum needed, disabled by -mod=readonly$'
+go get -d rsc.io/fortune@v1.0.0
+go install -mod=readonly $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
+exists $GOPATH/bin/fortune$GOEXE
+cd ..
+rm tmp
+rm $GOPATH/bin
+env GO111MODULE=auto
+
+# 'go install pkg@version' reports errors for meta packages, std packages,
+# and directories.
+! go install std@v1.0.0
+stderr '^go install std@v1.0.0: argument must be a package path, not a meta-package$'
+! go install fmt@v1.0.0
+stderr '^go install fmt@v1.0.0: argument must not be a package in the standard library$'
+! go install example.com//cmd/a@v1.0.0
+stderr '^go install example.com//cmd/a@v1.0.0: argument must be a clean package path$'
+! go install example.com/cmd/a@v1.0.0 ./x@v1.0.0
+stderr '^go install ./x@v1.0.0: argument must be a package path, not a relative path$'
+! go install example.com/cmd/a@v1.0.0 $GOPATH/src/x@v1.0.0
+stderr '^go install '$WORK'[/\\]gopath/src/x@v1.0.0: argument must be a package path, not an absolute path$'
+! go install example.com/cmd/a@v1.0.0 cmd/...@v1.0.0
+stderr '^go install: package cmd/go not provided by module example.com/cmd@v1.0.0$'
+
+# 'go install pkg@version' should accept multiple arguments but report an error
+# if the version suffixes are different, even if they refer to the same version.
+go install example.com/cmd/a@v1.0.0 example.com/cmd/b@v1.0.0
+exists $GOPATH/bin/a$GOEXE
+exists $GOPATH/bin/b$GOEXE
+rm $GOPATH/bin
+
+env GO111MODULE=on
+go list -m example.com/cmd@latest
+stdout '^example.com/cmd v1.0.0$'
+env GO111MODULE=auto
+
+! go install example.com/cmd/a@v1.0.0 example.com/cmd/b@latest
+stderr '^go install example.com/cmd/b@latest: all arguments must have the same version \(@v1.0.0\)$'
+
+
+# 'go install pkg@version' should report an error if the arguments are in
+# different modules.
+! go install example.com/cmd/a@v1.0.0 rsc.io/fortune@v1.0.0
+stderr '^go install: package rsc.io/fortune provided by module rsc.io/fortune@v1.0.0\n\tAll packages must be provided by the same module \(example.com/cmd@v1.0.0\).$'
+
+
+# 'go install pkg@version' should report an error if an argument is not
+# a main package.
+! go install example.com/cmd/a@v1.0.0 example.com/cmd/err@v1.0.0
+stderr '^go install: package example.com/cmd/err is not a main package$'
+
+# Wildcards should match only main packages. This module has a non-main package
+# with an error, so we'll know if that gets built.
+mkdir tmp
+cd tmp
+go mod init m
+go get -d example.com/cmd@v1.0.0
+! go build example.com/cmd/...
+stderr 'err[/\\]err.go:3:9: undefined: DoesNotCompile$'
+cd ..
+
+go install example.com/cmd/...@v1.0.0
+exists $GOPATH/bin/a$GOEXE
+exists $GOPATH/bin/b$GOEXE
+rm $GOPATH/bin
+
+# If a wildcard matches no packages, we should see a warning.
+! go install example.com/cmd/nomatch...@v1.0.0
+stderr '^go install example.com/cmd/nomatch\.\.\.@v1.0.0: module example.com/cmd@v1.0.0 found, but does not contain packages matching example.com/cmd/nomatch\.\.\.$'
+go install example.com/cmd/a@v1.0.0 example.com/cmd/nomatch...@v1.0.0
+stderr '^go: warning: "example.com/cmd/nomatch\.\.\." matched no packages$'
+
+# If a wildcard matches only non-main packges, we should see a different warning.
+go install example.com/cmd/err...@v1.0.0
+stderr '^go: warning: "example.com/cmd/err\.\.\." matched no main packages$'
+
+
+# 'go install pkg@version' should report errors if the module contains
+# replace or exclude directives.
+go mod download example.com/cmd@v1.0.0-replace
+! go install example.com/cmd/a@v1.0.0-replace
+cmp stderr replace-err
+
+go mod download example.com/cmd@v1.0.0-exclude
+! go install example.com/cmd/a@v1.0.0-exclude
+cmp stderr exclude-err
+
+# 'go install pkg@version' should report an error if the module requires a
+# higher version of itself.
+! go install example.com/cmd/a@v1.0.0-newerself
+stderr '^go install: example.com/cmd@v1.0.0-newerself: module requires a higher version of itself \(v1.0.0\)$'
+
+
+# 'go install pkg@version' will only match a retracted version if it's
+# explicitly requested.
+env GO111MODULE=on
+go list -m -versions example.com/cmd
+! stdout v1.9.0
+go list -m -versions -retracted example.com/cmd
+stdout v1.9.0
+go install example.com/cmd/a@latest
+go version -m $GOPATH/bin/a$GOEXE
+stdout '^\tmod\texample.com/cmd\tv1.0.0\t'
+go install example.com/cmd/a@v1.9.0
+go version -m $GOPATH/bin/a$GOEXE
+stdout '^\tmod\texample.com/cmd\tv1.9.0\t'
+
+-- m/go.mod --
+module m
+
+go 1.16
+
+require example.com/cmd v1.1.0-doesnotexist
+-- x/x.go --
+package main
+
+func main() {}
+-- replace-err --
+go install example.com/cmd/a@v1.0.0-replace: example.com/cmd@v1.0.0-replace
+	The go.mod file for the module providing named packages contains one or
+	more replace directives. It must not contain directives that would cause
+	it to be interpreted differently than if it were the main module.
+-- exclude-err --
+go install example.com/cmd/a@v1.0.0-exclude: example.com/cmd@v1.0.0-exclude
+	The go.mod file for the module providing named packages contains one or
+	more exclude directives. It must not contain directives that would cause
+	it to be interpreted differently than if it were the main module.
diff --git a/src/cmd/go/testdata/script/mod_outside.txt b/src/cmd/go/testdata/script/mod_outside.txt
index 03ef576..2001c45 100644
--- a/src/cmd/go/testdata/script/mod_outside.txt
+++ b/src/cmd/go/testdata/script/mod_outside.txt
@@ -177,9 +177,11 @@
 ! go doc example.com/version
 stderr 'doc: cannot find module providing package example.com/version: working directory is not part of a module'
 
-# 'go install' with a version should fail due to syntax.
-! go install example.com/printversion@v1.0.0
-stderr 'can only use path@version syntax with'
+# 'go install' with a version should succeed if all constraints are met.
+# See mod_install_pkg_version.
+rm $GOPATH/bin
+go install example.com/printversion@v0.1.0
+exists $GOPATH/bin/printversion$GOEXE
 
 # 'go install' should fail if a package argument must be resolved to a module.
 ! go install example.com/printversion