Skip to content

Commit 04e1176

Browse files
author
Jay Conrod
committed
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>
1 parent 639cb1b commit 04e1176

File tree

4 files changed

+184
-11
lines changed

4 files changed

+184
-11
lines changed

src/cmd/go/alldocs.go

+13-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/run/run.go

+67-7
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ package run
88
import (
99
"context"
1010
"fmt"
11+
"go/build"
1112
"os"
1213
"path"
14+
"path/filepath"
1315
"strings"
1416

1517
"cmd/go/internal/base"
1618
"cmd/go/internal/cfg"
1719
"cmd/go/internal/load"
20+
"cmd/go/internal/modload"
1821
"cmd/go/internal/str"
1922
"cmd/go/internal/work"
2023
)
@@ -24,10 +27,21 @@ var CmdRun = &base.Command{
2427
Short: "compile and run Go program",
2528
Long: `
2629
Run compiles and runs the named main Go package.
27-
Typically the package is specified as a list of .go source files from a single directory,
28-
but it may also be an import path, file system path, or pattern
30+
Typically the package is specified as a list of .go source files from a single
31+
directory, but it may also be an import path, file system path, or pattern
2932
matching a single known package, as in 'go run .' or 'go run my/cmd'.
3033
34+
If the package argument has a version suffix (like @latest or @v1.0.0),
35+
"go run" builds the program in module-aware mode, ignoring the go.mod file in
36+
the current directory or any parent directory, if there is one. This is useful
37+
for running programs without affecting the dependencies of the main module.
38+
39+
If the package argument doesn't have a version suffix, "go run" may run in
40+
module-aware mode or GOPATH mode, depending on the GO111MODULE environment
41+
variable and the presence of a go.mod file. See 'go help modules' for details.
42+
If module-aware mode is enabled, "go run" runs in the context of the main
43+
module.
44+
3145
By default, 'go run' runs the compiled binary directly: 'a.out arguments...'.
3246
If the -exec flag is given, 'go run' invokes the binary using xprog:
3347
'xprog a.out arguments...'.
@@ -59,10 +73,21 @@ func printStderr(args ...interface{}) (int, error) {
5973
}
6074

6175
func runRun(ctx context.Context, cmd *base.Command, args []string) {
76+
if shouldUseOutsideModuleMode(args) {
77+
// Set global module flags for 'go run cmd@version'.
78+
// This must be done before modload.Init, but we need to call work.BuildInit
79+
// before loading packages, since it affects package locations, e.g.,
80+
// for -race and -msan.
81+
modload.ForceUseModules = true
82+
modload.RootMode = modload.NoRoot
83+
modload.AllowMissingModuleImports()
84+
modload.Init()
85+
}
6286
work.BuildInit()
6387
var b work.Builder
6488
b.Init()
6589
b.Print = printStderr
90+
6691
i := 0
6792
for i < len(args) && strings.HasSuffix(args[i], ".go") {
6893
i++
@@ -79,28 +104,40 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
79104
}
80105
p = load.GoFilesPackage(ctx, load.PackageOpts{}, files)
81106
} else if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
82-
pkgs := load.PackagesAndErrors(ctx, load.PackageOpts{}, args[:1])
107+
arg := args[0]
108+
pkgOpts := load.PackageOpts{MainOnly: true}
109+
var pkgs []*load.Package
110+
if strings.Contains(arg, "@") && !build.IsLocalImport(arg) && !filepath.IsAbs(arg) {
111+
var err error
112+
pkgs, err = load.PackagesAndErrorsOutsideModule(ctx, pkgOpts, args[:1])
113+
if err != nil {
114+
base.Fatalf("go run: %v", err)
115+
}
116+
} else {
117+
pkgs = load.PackagesAndErrors(ctx, pkgOpts, args[:1])
118+
}
119+
83120
if len(pkgs) == 0 {
84-
base.Fatalf("go run: no packages loaded from %s", args[0])
121+
base.Fatalf("go run: no packages loaded from %s", arg)
85122
}
86123
if len(pkgs) > 1 {
87124
var names []string
88125
for _, p := range pkgs {
89126
names = append(names, p.ImportPath)
90127
}
91-
base.Fatalf("go run: pattern %s matches multiple packages:\n\t%s", args[0], strings.Join(names, "\n\t"))
128+
base.Fatalf("go run: pattern %s matches multiple packages:\n\t%s", arg, strings.Join(names, "\n\t"))
92129
}
93130
p = pkgs[0]
94131
i++
95132
} else {
96133
base.Fatalf("go run: no go files listed")
97134
}
98135
cmdArgs := args[i:]
99-
load.CheckPackageErrors([]*load.Package{p})
100-
101136
if p.Name != "main" {
102137
base.Fatalf("go run: cannot run non-main package")
103138
}
139+
load.CheckPackageErrors([]*load.Package{p})
140+
104141
p.Internal.OmitDebug = true
105142
p.Target = "" // must build - not up to date
106143
if p.Internal.CmdlineFiles {
@@ -123,11 +160,34 @@ func runRun(ctx context.Context, cmd *base.Command, args []string) {
123160
} else {
124161
p.Internal.ExeName = path.Base(p.ImportPath)
125162
}
163+
126164
a1 := b.LinkAction(work.ModeBuild, work.ModeBuild, p)
127165
a := &work.Action{Mode: "go run", Func: buildRunProgram, Args: cmdArgs, Deps: []*work.Action{a1}}
128166
b.Do(ctx, a)
129167
}
130168

169+
// shouldUseOutsideModuleMode returns whether 'go run' will load packages in
170+
// module-aware mode, ignoring the go.mod file in the current directory. It
171+
// returns true if the first argument contains "@", does not begin with "-"
172+
// (resembling a flag) or end with ".go" (a file). The argument must not be a
173+
// local or absolute file path.
174+
//
175+
// These rules are slightly different than other commands. Whether or not
176+
// 'go run' uses this mode, it interprets arguments ending with ".go" as files
177+
// and uses arguments up to the last ".go" argument to comprise the package.
178+
// If there are no ".go" arguments, only the first argument is interpreted
179+
// as a package path, since there can be only one package.
180+
func shouldUseOutsideModuleMode(args []string) bool {
181+
// NOTE: "@" not allowed in import paths, but it is allowed in non-canonical
182+
// versions.
183+
return len(args) > 0 &&
184+
!strings.HasSuffix(args[0], ".go") &&
185+
!strings.HasPrefix(args[0], "-") &&
186+
strings.Contains(args[0], "@") &&
187+
!build.IsLocalImport(args[0]) &&
188+
!filepath.IsAbs(args[0])
189+
}
190+
131191
// buildRunProgram is the action for running a binary that has already
132192
// been compiled. We ignore exit status.
133193
func buildRunProgram(b *work.Builder, ctx context.Context, a *work.Action) error {

src/cmd/go/testdata/mod/example.com_cmd_v1.0.0.txt

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ go 1.16
1616
-- a/a.go --
1717
package main
1818

19-
func main() {}
19+
import "fmt"
20+
21+
func main() { fmt.Println("a@v1.0.0") }
2022
-- b/b.go --
2123
package main
2224

23-
func main() {}
25+
import "fmt"
26+
27+
func main() { fmt.Println("b@v1.0.0") }
2428
-- err/err.go --
2529
package err
2630

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# This test checks the behavior of 'go run' with a 'cmd@version' argument.
2+
# Most of 'go run' is covered in other tests.
3+
# mod_install_pkg_version covers most of the package loading functionality.
4+
# This test focuses on 'go run' behavior specific to this mode.
5+
[short] skip
6+
7+
# 'go run pkg@version' works outside a module.
8+
env GO111MODULE=auto
9+
go run example.com/cmd/a@v1.0.0
10+
stdout '^a@v1.0.0$'
11+
12+
13+
# 'go run pkg@version' reports an error if modules are disabled.
14+
env GO111MODULE=off
15+
! go run example.com/cmd/a@v1.0.0
16+
stderr '^go: modules disabled by GO111MODULE=off; see ''go help modules''$'
17+
env GO111MODULE=on
18+
19+
20+
# 'go run pkg@version' ignores go.mod in the current directory.
21+
cd m
22+
cp go.mod go.mod.orig
23+
! go list -m all
24+
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$'
25+
go run example.com/cmd/a@v1.0.0
26+
stdout '^a@v1.0.0$'
27+
cmp go.mod go.mod.orig
28+
cd ..
29+
30+
31+
# 'go install pkg@version' works on a module that doesn't have a go.mod file
32+
# and with a module whose go.mod file has missing requirements.
33+
# With a proxy, the two cases are indistinguishable.
34+
go run rsc.io/fortune@v1.0.0
35+
stderr '^go: found rsc.io/quote in rsc.io/quote v1.5.2$'
36+
stderr '^Hello, world.$'
37+
38+
39+
# 'go run pkg@version' should report errors if the module contains
40+
# replace or exclude directives.
41+
go mod download example.com/cmd@v1.0.0-replace
42+
! go run example.com/cmd/a@v1.0.0-replace
43+
cmp stderr replace-err
44+
45+
go mod download example.com/cmd@v1.0.0-exclude
46+
! go run example.com/cmd/a@v1.0.0-exclude
47+
cmp stderr exclude-err
48+
49+
50+
# 'go run dir@version' works like a normal 'go run' command if
51+
# dir is a relative or absolute path.
52+
go mod download rsc.io/fortune@v1.0.0
53+
! go run $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
54+
stderr '^go: go\.mod file not found in current directory or any parent directory; see ''go help modules''$'
55+
! go run ../pkg/mod/rsc.io/fortune@v1.0.0
56+
stderr '^go: go\.mod file not found in current directory or any parent directory; see ''go help modules''$'
57+
mkdir tmp
58+
cd tmp
59+
go mod init tmp
60+
go mod edit -require=rsc.io/fortune@v1.0.0
61+
! go run -mod=readonly $GOPATH/pkg/mod/rsc.io/fortune@v1.0.0
62+
stderr '^go: rsc.io/fortune@v1.0.0: missing go.sum entry; to add it:\n\tgo mod download rsc.io/fortune$'
63+
! go run -mod=readonly ../../pkg/mod/rsc.io/fortune@v1.0.0
64+
stderr '^go: rsc.io/fortune@v1.0.0: missing go.sum entry; to add it:\n\tgo mod download rsc.io/fortune$'
65+
cd ..
66+
rm tmp
67+
68+
69+
# 'go run' does not interpret @version arguments after the first.
70+
go run example.com/cmd/a@v1.0.0 example.com/doesnotexist@v1.0.0
71+
stdout '^a@v1.0.0$'
72+
73+
74+
# 'go run pkg@version' succeeds when -mod=readonly is set explicitly.
75+
# Verifies #43278.
76+
go run -mod=readonly example.com/cmd/a@v1.0.0
77+
stdout '^a@v1.0.0$'
78+
79+
-- m/go.mod --
80+
module m
81+
82+
go 1.16
83+
84+
require example.com/cmd v1.1.0-doesnotexist
85+
-- x/x.go --
86+
package main
87+
88+
func main() {}
89+
-- replace-err --
90+
go run: example.com/cmd/a@v1.0.0-replace (in example.com/cmd@v1.0.0-replace):
91+
The go.mod file for the module providing named packages contains one or
92+
more replace directives. It must not contain directives that would cause
93+
it to be interpreted differently than if it were the main module.
94+
-- exclude-err --
95+
go run: example.com/cmd/a@v1.0.0-exclude (in example.com/cmd@v1.0.0-exclude):
96+
The go.mod file for the module providing named packages contains one or
97+
more exclude directives. It must not contain directives that would cause
98+
it to be interpreted differently than if it were the main module.

0 commit comments

Comments
 (0)