Skip to content

Commit

Permalink
cmd/go/internal/toolchain: make a best effort to parse 'go run' and '…
Browse files Browse the repository at this point in the history
…go install' flags

When the argument to 'go install' or 'go run' looks like a versioned
package, we make a best effort to switch to a toolchain compatible
with the module containing that package, by fetching its go.mod file
and checking the go version it specifies.

At this point in the code, we have not yet parsed the arguments given
on the command line: instead, we just make a best effort to find one
we can use to select a toolchain version. Since that toolchain may be
newer, the command to install it may also include flags that are only
supported by that Go version — and we don't want to fail due to an
error that would be resolved by switching to a more appropriate
toolchain.

So at this point in the code we can't parse the flags in a way that
will surface errors, but we want to make a best effort to parse the
ones that we know about. It turns out that “parse the flags we know
about” is already a familiar problem: that's also what we do in
'go test', so we can reuse the cmdflag library from that to do the
best-effort pass of parsing.

If it turns out that we don't need to switch toolchains after all,
cmd/go's main function will parse the flags again, and will report any
errors at that point.

This fixes a regression, introduced in CL 497879, which caused
'go install -modcacherw pkg@version' to unset the write bit for
directories created while selecting the toolchain to use.

Fixes golang#64282.
Updates golang#57001.

Change-Id: Icc409c57858aa15c7d58a97a61964b4bc2560547
Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest
Reviewed-on: https://go-review.googlesource.com/c/go/+/546635
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
  • Loading branch information
Bryan C. Mills authored and gopherbot committed Dec 14, 2023
1 parent 7c282ba commit e44b8b1
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 45 deletions.
10 changes: 5 additions & 5 deletions src/cmd/go/internal/base/goflags.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ type boolFlag interface {
}

// SetFromGOFLAGS sets the flags in the given flag set using settings in $GOFLAGS.
func SetFromGOFLAGS(flags *flag.FlagSet) {
func SetFromGOFLAGS(flags *flag.FlagSet, ignoreErrors bool) {
InitGOFLAGS()

// This loop is similar to flag.Parse except that it ignores
Expand Down Expand Up @@ -121,22 +121,22 @@ func SetFromGOFLAGS(flags *flag.FlagSet) {

if fb, ok := f.Value.(boolFlag); ok && fb.IsBoolFlag() {
if hasValue {
if err := flags.Set(f.Name, value); err != nil {
if err := flags.Set(f.Name, value); err != nil && !ignoreErrors {
fmt.Fprintf(flags.Output(), "go: invalid boolean value %q for flag %s (from %s): %v\n", value, name, where, err)
flags.Usage()
}
} else {
if err := flags.Set(f.Name, "true"); err != nil {
if err := flags.Set(f.Name, "true"); err != nil && !ignoreErrors {
fmt.Fprintf(flags.Output(), "go: invalid boolean flag %s (from %s): %v\n", name, where, err)
flags.Usage()
}
}
} else {
if !hasValue {
if !hasValue && !ignoreErrors {
fmt.Fprintf(flags.Output(), "go: flag needs an argument: %s (from %s)\n", name, where)
flags.Usage()
}
if err := flags.Set(f.Name, value); err != nil {
if err := flags.Set(f.Name, value); err != nil && !ignoreErrors {
fmt.Fprintf(flags.Output(), "go: invalid value %q for flag %s (from %s): %v\n", value, name, where, err)
flags.Usage()
}
Expand Down
20 changes: 10 additions & 10 deletions src/cmd/go/internal/modfetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func readGoSum(dst map[module.Version][]string, file string, data []byte) {
// ignore malformed line so that go mod tidy can fix go.sum
continue
} else {
base.Fatalf("malformed go.sum:\n%s:%d: wrong number of fields %v\n", file, lineno, len(f))
base.Fatalf("go: malformed go.sum:\n%s:%d: wrong number of fields %v\n", file, lineno, len(f))
}
}
if f[2] == emptyGoModHash {
Expand Down Expand Up @@ -574,32 +574,32 @@ func checkMod(ctx context.Context, mod module.Version) {
// Do the file I/O before acquiring the go.sum lock.
ziphash, err := CachePath(ctx, mod, "ziphash")
if err != nil {
base.Fatalf("verifying %v", module.VersionError(mod, err))
base.Fatalf("go: verifying %v", module.VersionError(mod, err))
}
data, err := lockedfile.Read(ziphash)
if err != nil {
base.Fatalf("verifying %v", module.VersionError(mod, err))
base.Fatalf("go: verifying %v", module.VersionError(mod, err))
}
data = bytes.TrimSpace(data)
if !isValidSum(data) {
// Recreate ziphash file from zip file and use that to check the mod sum.
zip, err := CachePath(ctx, mod, "zip")
if err != nil {
base.Fatalf("verifying %v", module.VersionError(mod, err))
base.Fatalf("go: verifying %v", module.VersionError(mod, err))
}
err = hashZip(mod, zip, ziphash)
if err != nil {
base.Fatalf("verifying %v", module.VersionError(mod, err))
base.Fatalf("go: verifying %v", module.VersionError(mod, err))
}
return
}
h := string(data)
if !strings.HasPrefix(h, "h1:") {
base.Fatalf("verifying %v", module.VersionError(mod, fmt.Errorf("unexpected ziphash: %q", h)))
base.Fatalf("go: verifying %v", module.VersionError(mod, fmt.Errorf("unexpected ziphash: %q", h)))
}

if err := checkModSum(mod, h); err != nil {
base.Fatalf("%s", err)
base.Fatal(err)
}
}

Expand Down Expand Up @@ -684,7 +684,7 @@ func haveModSumLocked(mod module.Version, h string) bool {
return true
}
if strings.HasPrefix(vh, "h1:") {
base.Fatalf("verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, sumFileName, vh)
base.Fatalf("go: verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, sumFileName, vh)
}
}
// Also check workspace sums.
Expand All @@ -696,7 +696,7 @@ func haveModSumLocked(mod module.Version, h string) bool {
if h == vh {
foundMatch = true
} else if strings.HasPrefix(vh, "h1:") {
base.Fatalf("verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, goSumFile, vh)
base.Fatalf("go: verifying %s@%s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+goSumMismatch, mod.Path, mod.Version, h, goSumFile, vh)
}
}
}
Expand Down Expand Up @@ -895,7 +895,7 @@ func TrimGoSum(keep map[module.Version]bool) {
defer goSum.mu.Unlock()
inited, err := initGoSum()
if err != nil {
base.Fatalf("%s", err)
base.Fatal(err)
}
if !inited {
return
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/go/internal/test/testflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ func (f *shuffleFlag) Set(value string) error {
// go test fmt -custom-flag-for-fmt-test
// go test -x math
func testFlags(args []string) (packageNames, passToTest []string) {
base.SetFromGOFLAGS(&CmdTest.Flag)
base.SetFromGOFLAGS(&CmdTest.Flag, false)
addFromGOFLAGS := map[string]bool{}
CmdTest.Flag.Visit(func(f *flag.Flag) {
if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
Expand Down
6 changes: 3 additions & 3 deletions src/cmd/go/internal/toolchain/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ func execGoToolchain(gotoolchain, dir, exe string) {
if e.ProcessState.Exited() {
os.Exit(e.ProcessState.ExitCode())
}
base.Fatalf("exec %s: %s", gotoolchain, e.ProcessState)
base.Fatalf("go: exec %s: %s", gotoolchain, e.ProcessState)
}
base.Fatalf("exec %s: %s", exe, err)
base.Fatalf("go: exec %s: %s", exe, err)
}
os.Exit(0)
}
err := syscall.Exec(exe, os.Args, os.Environ())
base.Fatalf("exec %s: %v", gotoolchain, err)
base.Fatalf("go: exec %s: %v", gotoolchain, err)
}
56 changes: 35 additions & 21 deletions src/cmd/go/internal/toolchain/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ package toolchain
import (
"context"
"errors"
"flag"
"fmt"
"go/build"
"io/fs"
"log"
"os"
"path/filepath"
"runtime"
Expand All @@ -20,10 +20,12 @@ import (

"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/cmdflag"
"cmd/go/internal/gover"
"cmd/go/internal/modfetch"
"cmd/go/internal/modload"
"cmd/go/internal/run"
"cmd/go/internal/work"

"golang.org/x/mod/module"
)
Expand Down Expand Up @@ -85,9 +87,6 @@ func FilterEnv(env []string) []string {
// It must be called early in startup.
// See https://go.dev/doc/toolchain#select.
func Select() {
log.SetPrefix("go: ")
defer log.SetPrefix("")

if !modload.WillBeEnabled() {
return
}
Expand Down Expand Up @@ -133,15 +132,15 @@ func Select() {
v := gover.FromToolchain(min)
if v == "" {
if plus {
base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", gotoolchain, min)
base.Fatalf("go: invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", gotoolchain, min)
}
base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain)
base.Fatalf("go: invalid GOTOOLCHAIN %q", gotoolchain)
}
minToolchain = min
minVers = v
}
if plus && suffix != "auto" && suffix != "path" {
base.Fatalf("invalid GOTOOLCHAIN %q: only version suffixes are +auto and +path", gotoolchain)
base.Fatalf("go: invalid GOTOOLCHAIN %q: only version suffixes are +auto and +path", gotoolchain)
}
mode = suffix
}
Expand Down Expand Up @@ -172,7 +171,7 @@ func Select() {
// has a suffix like "go1.21.1-foo" and toolchain is "go1.21.1".)
toolVers := gover.FromToolchain(toolchain)
if toolVers == "" || (!strings.HasPrefix(toolchain, "go") && !strings.Contains(toolchain, "-go")) {
base.Fatalf("invalid toolchain %q in %s", toolchain, base.ShortPath(file))
base.Fatalf("go: invalid toolchain %q in %s", toolchain, base.ShortPath(file))
}
if gover.Compare(toolVers, minVers) > 0 {
gotoolchain = toolchain
Expand All @@ -194,7 +193,7 @@ func Select() {
// so that we have initialized gover.Startup for use in error messages.
if target := os.Getenv(targetEnv); target != "" && TestVersionSwitch != "loop" {
if gover.LocalToolchain() != target {
base.Fatalf("toolchain %v invoked to provide %v", gover.LocalToolchain(), target)
base.Fatalf("go: toolchain %v invoked to provide %v", gover.LocalToolchain(), target)
}
os.Unsetenv(targetEnv)

Expand Down Expand Up @@ -225,7 +224,7 @@ func Select() {
// We want to disallow mistakes / bad ideas like GOTOOLCHAIN=bash,
// since we will find that in the path lookup.
if !strings.HasPrefix(gotoolchain, "go1") && !strings.Contains(gotoolchain, "-go1") {
base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain)
base.Fatalf("go: invalid GOTOOLCHAIN %q", gotoolchain)
}

Exec(gotoolchain)
Expand All @@ -244,16 +243,14 @@ var TestVersionSwitch string
// as a source of Go toolchains. Otherwise Exec tries the PATH but then downloads
// a toolchain if necessary.
func Exec(gotoolchain string) {
log.SetPrefix("go: ")

writeBits = sysWriteBits()

count, _ := strconv.Atoi(os.Getenv(countEnv))
if count >= maxSwitch-10 {
fmt.Fprintf(os.Stderr, "go: switching from go%v to %v [depth %d]\n", gover.Local(), gotoolchain, count)
}
if count >= maxSwitch {
base.Fatalf("too many toolchain switches")
base.Fatalf("go: too many toolchain switches")
}
os.Setenv(countEnv, fmt.Sprint(count+1))

Expand All @@ -276,7 +273,7 @@ func Exec(gotoolchain string) {
case "loop", "mismatch":
exe, err := os.Executable()
if err != nil {
base.Fatalf("%v", err)
base.Fatal(err)
}
execGoToolchain(gotoolchain, os.Getenv("GOROOT"), exe)
}
Expand All @@ -291,7 +288,7 @@ func Exec(gotoolchain string) {
// GOTOOLCHAIN=auto looks in PATH and then falls back to download.
// GOTOOLCHAIN=path only looks in PATH.
if pathOnly {
base.Fatalf("cannot find %q in PATH", gotoolchain)
base.Fatalf("go: cannot find %q in PATH", gotoolchain)
}

// Set up modules without an explicit go.mod, to download distribution.
Expand All @@ -310,9 +307,9 @@ func Exec(gotoolchain string) {
dir, err := modfetch.Download(context.Background(), m)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
base.Fatalf("download %s for %s/%s: toolchain not available", gotoolchain, runtime.GOOS, runtime.GOARCH)
base.Fatalf("go: download %s for %s/%s: toolchain not available", gotoolchain, runtime.GOOS, runtime.GOARCH)
}
base.Fatalf("download %s: %v", gotoolchain, err)
base.Fatalf("go: download %s: %v", gotoolchain, err)
}

// On first use after download, set the execute bits on the commands
Expand All @@ -321,7 +318,7 @@ func Exec(gotoolchain string) {
if runtime.GOOS != "windows" {
info, err := os.Stat(filepath.Join(dir, "bin/go"))
if err != nil {
base.Fatalf("download %s: %v", gotoolchain, err)
base.Fatalf("go: download %s: %v", gotoolchain, err)
}
if info.Mode()&0111 == 0 {
// allowExec sets the exec permission bits on all files found in dir.
Expand All @@ -342,7 +339,7 @@ func Exec(gotoolchain string) {
return nil
})
if err != nil {
base.Fatalf("download %s: %v", gotoolchain, err)
base.Fatalf("go: download %s: %v", gotoolchain, err)
}
}

Expand Down Expand Up @@ -384,7 +381,7 @@ func Exec(gotoolchain string) {
err = raceSafeCopy(srcUGoMod, srcGoMod)
}
if err != nil {
base.Fatalf("download %s: %v", gotoolchain, err)
base.Fatalf("go: download %s: %v", gotoolchain, err)
}
}

Expand Down Expand Up @@ -475,7 +472,7 @@ func modGoToolchain() (file, goVers, toolchain string) {

data, err := os.ReadFile(file)
if err != nil {
base.Fatalf("%v", err)
base.Fatal(err)
}
return file, gover.GoModLookup(data, "go"), gover.GoModLookup(data, "toolchain")
}
Expand All @@ -492,6 +489,7 @@ func goInstallVersion() bool {

// Check for pkg@version.
var arg string
var cmdFlags *flag.FlagSet
switch os.Args[1] {
default:
return false
Expand All @@ -500,13 +498,15 @@ func goInstallVersion() bool {
// across a toolchain switch. To make that work, assume the pkg@version
// is the last argument and skip the flag parsing.
arg = os.Args[len(os.Args)-1]
cmdFlags = &work.CmdInstall.Flag
case "run":
// For run, the pkg@version can be anywhere on the command line,
// because it is preceded by run flags and followed by arguments to the
// program being run. To handle that precisely, we have to interpret the
// flags a little bit, to know whether each flag takes an optional argument.
// We can still allow unknown flags as long as they have an explicit =value.
args := os.Args[2:]
cmdFlags = &run.CmdRun.Flag
for i := 0; i < len(args); i++ {
a := args[i]
if !strings.HasPrefix(a, "-") {
Expand Down Expand Up @@ -554,6 +554,20 @@ func goInstallVersion() bool {
return false
}

// Make a best effort to parse flags so that module flags like -modcacherw
// will take effect (see https://go.dev/issue/64282).
args := os.Args[2:]
for len(args) > 0 {
var err error
_, args, err = cmdflag.ParseOne(cmdFlags, args)
if errors.Is(err, cmdflag.ErrFlagTerminator) {
break
}
// Ignore all other errors: they may be new flags — or updated syntax for
// existing flags — intended for a newer Go toolchain.
}
base.SetFromGOFLAGS(cmdFlags, true)

// It would be correct to simply return true here, bypassing use
// of the current go.mod or go.work, and let "go run" or "go install"
// do the rest, including a toolchain switch.
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/go/internal/vet/vetflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func vetFlags(args []string) (passToVet, packageNames []string) {

// Record the set of vet tool flags set by GOFLAGS. We want to pass them to
// the vet tool, but only if they aren't overridden by an explicit argument.
base.SetFromGOFLAGS(&CmdVet.Flag)
base.SetFromGOFLAGS(&CmdVet.Flag, false)
addFromGOFLAGS := map[string]bool{}
CmdVet.Flag.Visit(func(f *flag.Flag) {
if isVetFlag[f.Name] {
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func invoke(cmd *base.Command, args []string) {
if cmd.CustomFlags {
args = args[1:]
} else {
base.SetFromGOFLAGS(&cmd.Flag)
base.SetFromGOFLAGS(&cmd.Flag, false)
cmd.Flag.Parse(args[1:])
args = cmd.Flag.Args()
}
Expand Down
32 changes: 32 additions & 0 deletions src/cmd/go/testdata/script/install_modcacherw_issue64282.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Regression test for https://go.dev/issue/64282:
# 'go install' and 'go run' with pkg@version arguments should make
# a best effort to parse flags before they download modules to
# identify which toolchain version to use, because those flags
# may affect the downloaded contents.

# However, the best-effort flag parsing should not interfere with
# actual flag parsing if we don't switch toolchains. In particular,
# unrecognized flags should still be diagnosed after the module for
# the requested package has been downloaded and checked for toolchain
# upgrades.

! go install -cake=delicious -modcacherw example.com/printversion@v0.1.0
stderr '^flag provided but not defined: -cake$'

[!short] go install -modcacherw example.com/printversion@v0.1.0
# Because the -modcacherw flag was set, we should be able to modify the contents
# of a directory within the module cache.
cp $WORK/extraneous.txt $GOPATH/pkg/mod/example.com/printversion@v0.1.0/extraneous_file.go


# We should also apply flags from GOFLAGS at this step.

go clean -modcache
env GOFLAGS=-modcacherw
! go install -cake=delicious example.com/printversion@v0.1.0
stderr '^flag provided but not defined: -cake$'
cp $WORK/extraneous.txt $GOPATH/pkg/mod/example.com/printversion@v0.1.0/extraneous_file.go


-- $WORK/extraneous.txt --
This is not a Go source file.
Loading

0 comments on commit e44b8b1

Please sign in to comment.