]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: support 'go run cmd@version'
authorJay Conrod <jayconrod@google.com>
Tue, 13 Apr 2021 22:00:09 +0000 (18:00 -0400)
committerJay Conrod <jayconrod@google.com>
Fri, 16 Apr 2021 16:11:09 +0000 (16:11 +0000)
'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>
src/cmd/go/alldocs.go
src/cmd/go/internal/run/run.go
src/cmd/go/testdata/mod/example.com_cmd_v1.0.0.txt
src/cmd/go/testdata/script/mod_run_pkg_version.txt [new file with mode: 0644]

index 66e78bb1ace8fcf1c6cccd19235c76cddc42ad9a..999fcf7e531f2605bbef4fc6ee73eae148e59adf 100644 (file)
 //     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...'.
index f0137c20c19fc7d0fdcb702eb3a0b6208d094dab..914e5edc6f24bfdfd5c5834cda226effa8e30456 100644 (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 {
index ee439384d25817bb23ad7e6f09cc835ca8a9eae8..c1981391a13c74a57b1a326f4cd915b236594208 100644 (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
 
diff --git a/src/cmd/go/testdata/script/mod_run_pkg_version.txt b/src/cmd/go/testdata/script/mod_run_pkg_version.txt
new file mode 100644 (file)
index 0000000..d96d3fc
--- /dev/null
@@ -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.