cmd/go: support 'go run cmd@version'

'go run' can now build a command at a specific version in module-aware
mode, ignoring the go.mod file in the current directory if there is one.

For #42088

Change-Id: I0bd9bcbe40c0442a268cd1cc315a8a2cbb5adeee
Reviewed-on: https://go-review.googlesource.com/c/go/+/310074
Trust: Jay Conrod <jayconrod@google.com>
Run-TryBot: Jay Conrod <jayconrod@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Bryan C. Mills <bcmills@google.com>
This commit is contained in:
Jay Conrod 2021-04-13 18:00:09 -04:00
parent 639cb1b629
commit 04e1176fd2
4 changed files with 184 additions and 11 deletions

View file

@ -1315,10 +1315,21 @@
// go run [build flags] [-exec xprog] package [arguments...]
//
// Run compiles and runs the named main Go package.
// Typically the package is specified as a list of .go source files from a single directory,
// but it may also be an import path, file system path, or pattern
// Typically the package is specified as a list of .go source files from a single
// directory, but it may also be an import path, file system path, or pattern
// matching a single known package, as in 'go run .' or 'go run my/cmd'.
//
// If the package argument has a version suffix (like @latest or @v1.0.0),
// "go run" builds the program 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 running programs without affecting the dependencies of the main module.
//
// If the package argument doesn't have a version suffix, "go run" 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 run" runs in the context of the main
// module.
//
// By default, 'go run' runs the compiled binary directly: 'a.out arguments...'.
// If the -exec flag is given, 'go run' invokes the binary using xprog:
// 'xprog a.out arguments...'.

View file

@ -8,13 +8,16 @@ package run
import (
"context"
"fmt"
"go/build"
"os"
"path"
"path/filepath"
"strings"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/load"
"cmd/go/internal/modload"
"cmd/go/internal/str"
"cmd/go/internal/work"
)
@ -24,10 +27,21 @@ var CmdRun = &base.Command{
Short: "compile and run Go program",
Long: `
Run compiles and runs the named main Go package.
Typically the package is specified as a list of .go source files from a single directory,
but it may also be an import path, file system path, or pattern
Typically the package is specified as a list of .go source files from a single
directory, but it may also be an import path, file system path, or pattern
matching a single known package, as in 'go run .' or 'go run my/cmd'.
If the package argument has a version suffix (like @latest or @v1.0.0),
"go run" builds the program 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 running programs without affecting the dependencies of the main module.
If the package argument doesn't have a version suffix, "go run" 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 run" runs in the context of the main
module.
By default, 'go run' runs the compiled binary directly: 'a.out arguments...'.
If the -exec flag is given, 'go run' invokes the binary using xprog:
'xprog a.out arguments...'.
@ -59,10 +73,21 @@ func printStderr(args ...interface{}) (int, error) {
}
func runRun(ctx context.Context, cmd *base.Command, args []string) {
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
// before loading packages, since it affects package locations, e.g.,
// for -race and -msan.
modload.ForceUseModules = true
modload.RootMode = modload.NoRoot
modload.AllowMissingModuleImports()
modload.Init()
}
work.BuildInit()
var b work.Builder
b.Init()
b.Print = printStderr
i := 0
for i < len(args) && strings.HasSuffix(args[i], ".go") {
i++
@ -79,16 +104,28 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
}
p = load.GoFilesPackage(ctx, load.PackageOpts{}, files)
} else if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{}, args[:1])
arg := args[0]
pkgOpts := load.PackageOpts{MainOnly: true}
var pkgs []*load.Package
if strings.Contains(arg, "@") && !build.IsLocalImport(arg) && !filepath.IsAbs(arg) {
var err error
pkgs, err = load.PackagesAndErrorsOutsideModule(ctx, pkgOpts, args[:1])
if err != nil {
base.Fatalf("go run: %v", err)
}
} else {
pkgs = load.PackagesAndErrors(ctx, pkgOpts, args[:1])
}
if len(pkgs) == 0 {
base.Fatalf("go run: no packages loaded from %s", args[0])
base.Fatalf("go run: no packages loaded from %s", arg)
}
if len(pkgs) > 1 {
var names []string
for _, p := range pkgs {
names = append(names, p.ImportPath)
}
base.Fatalf("go run: pattern %s matches multiple packages:\n\t%s", args[0], strings.Join(names, "\n\t"))
base.Fatalf("go run: pattern %s matches multiple packages:\n\t%s", arg, strings.Join(names, "\n\t"))
}
p = pkgs[0]
i++
@ -96,11 +133,11 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
base.Fatalf("go run: no go files listed")
}
cmdArgs := args[i:]
load.CheckPackageErrors([]*load.Package{p})
if p.Name != "main" {
base.Fatalf("go run: cannot run non-main package")
}
load.CheckPackageErrors([]*load.Package{p})
p.Internal.OmitDebug = true
p.Target = "" // must build - not up to date
if p.Internal.CmdlineFiles {
@ -123,11 +160,34 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
} else {
p.Internal.ExeName = path.Base(p.ImportPath)
}
a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}
b.Do(ctx, a)
}
// shouldUseOutsideModuleMode returns whether 'go run' will load packages in
// module-aware mode, ignoring the go.mod file in the current directory. It
// returns true if the first argument contains "@", does not begin with "-"
// (resembling a flag) or end with ".go" (a file). The argument must not be a
// local or absolute file path.
//
// These rules are slightly different than other commands. Whether or not
// 'go run' uses this mode, it interprets arguments ending with ".go" as files
// and uses arguments up to the last ".go" argument to comprise the package.
// If there are no ".go" arguments, only the first argument is interpreted
// as a package path, since there can be only one package.
func shouldUseOutsideModuleMode(args []string) bool {
// NOTE: "@" not allowed in import paths, but it is allowed in non-canonical
// versions.
return len(args) > 0 &&
!strings.HasSuffix(args[0], ".go") &&
!strings.HasPrefix(args[0], "-") &&
strings.Contains(args[0], "@") &&
!build.IsLocalImport(args[0]) &&
!filepath.IsAbs(args[0])
}
// buildRunProgram is the action for running a binary that has already
// been compiled. We ignore exit status.
func buildRunProgram(b *work.Builder, ctx context.Context, a *work.Action) error {

View file

@ -16,11 +16,15 @@ go 1.16
-- a/a.go --
package main
func main() {}
import "fmt"
func main() { fmt.Println("a@v1.0.0") }
-- b/b.go --
package main
func main() {}
import "fmt"
func main() { fmt.Println("b@v1.0.0") }
-- err/err.go --
package err

View file

@ -0,0 +1,98 @@
# This test checks the behavior of 'go run' with a 'cmd@version' argument.
# Most of 'go run' is covered in other tests.
# mod_install_pkg_version covers most of the package loading functionality.
# This test focuses on 'go run' behavior specific to this mode.
[short] skip
# 'go run pkg@version' works outside a module.
env GO111MODULE=auto
go run example.com/cmd/a@v1.0.0
stdout '^a@v1.0.0$'
# 'go run pkg@version' reports an error if modules are disabled.
env GO111MODULE=off
! go run example.com/cmd/a@v1.0.0
stderr '^go: modules disabled by GO111MODULE=off; see ''go help modules''$'
env GO111MODULE=on
# 'go run pkg@version' ignores go.mod in the current directory.
cd m
cp go.mod go.mod.orig
! go list -m all
stderr '^go list -m: example.com/cmd@v1.1.0-doesnotexist: missing go.sum entry; to add it:\n\tgo mod download example.com/cmd$'
go run example.com/cmd/a@v1.0.0
stdout '^a@v1.0.0$'
cmp go.mod go.mod.orig
cd ..
# '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 run rsc.io/fortune@v1.0.0
stderr '^go: found rsc.io/quote in rsc.io/quote v1.5.2$'
stderr '^Hello, world.$'
# 'go run pkg@version' should report errors if the module contains
# replace or exclude directives.
go mod download example.com/cmd@v1.0.0-replace
! go run example.com/cmd/a@v1.0.0-replace
cmp stderr replace-err
go mod download example.com/cmd@v1.0.0-exclude
! go run example.com/cmd/a@v1.0.0-exclude
cmp stderr exclude-err
# 'go run dir@version' works like a normal 'go run' command if
# dir is a relative or absolute path.
go mod download rsc.io/fortune@v1.0.0
! go run $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
stderr '^go: go\.mod file not found in current directory or any parent directory; see ''go help modules''$'
! go run ../pkg/mod/rsc.io/fortune@v1.0.0
stderr '^go: go\.mod file not found in current directory or any parent directory; see ''go help modules''$'
mkdir tmp
cd tmp
go mod init tmp
go mod edit -require=rsc.io/fortune@v1.0.0
! go run -mod=readonly $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
stderr '^go: rsc.io/fortune@v1.0.0: missing go.sum entry; to add it:\n\tgo mod download rsc.io/fortune$'
! go run -mod=readonly ../../pkg/mod/rsc.io/fortune@v1.0.0
stderr '^go: rsc.io/fortune@v1.0.0: missing go.sum entry; to add it:\n\tgo mod download rsc.io/fortune$'
cd ..
rm tmp
# 'go run' does not interpret @version arguments after the first.
go run example.com/cmd/a@v1.0.0 example.com/doesnotexist@v1.0.0
stdout '^a@v1.0.0$'
# 'go run pkg@version' succeeds when -mod=readonly is set explicitly.
# Verifies #43278.
go run -mod=readonly example.com/cmd/a@v1.0.0
stdout '^a@v1.0.0$'
-- 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 run: example.com/cmd/a@v1.0.0-replace (in 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 run: example.com/cmd/a@v1.0.0-exclude (in 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.