github.com/bir3/gocompiler@v0.9.2202/src/cmd/gocmd/internal/toolchain/select.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package toolchain implements dynamic switching of Go toolchains.
     6  package toolchain
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"github.com/bir3/gocompiler/src/cmd/gocmd/flag"
    12  	"fmt"
    13  	"github.com/bir3/gocompiler/src/go/build"
    14  	"io/fs"
    15  	"log"
    16  	"os"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/base"
    23  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/cfg"
    24  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/gover"
    25  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/modfetch"
    26  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/modload"
    27  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/run"
    28  	"github.com/bir3/gocompiler/src/cmd/gocmd/internal/work"
    29  
    30  	"github.com/bir3/gocompiler/src/xvendor/golang.org/x/mod/module"
    31  )
    32  
    33  const (
    34  	// We download golang.org/toolchain version v0.0.1-<gotoolchain>.<goos>-<goarch>.
    35  	// If the 0.0.1 indicates anything at all, its the version of the toolchain packaging:
    36  	// if for some reason we needed to change the way toolchains are packaged into
    37  	// module zip files in a future version of Go, we could switch to v0.0.2 and then
    38  	// older versions expecting the old format could use v0.0.1 and newer versions
    39  	// would use v0.0.2. Of course, then we'd also have to publish two of each
    40  	// module zip file. It's not likely we'll ever need to change this.
    41  	gotoolchainModule	= "golang.org/toolchain"
    42  	gotoolchainVersion	= "v0.0.1"
    43  
    44  	// targetEnv is a special environment variable set to the expected
    45  	// toolchain version during the toolchain switch by the parent
    46  	// process and cleared in the child process. When set, that indicates
    47  	// to the child to confirm that it provides the expected toolchain version.
    48  	targetEnv	= "GOTOOLCHAIN_INTERNAL_SWITCH_VERSION"
    49  
    50  	// countEnv is a special environment variable
    51  	// that is incremented during each toolchain switch, to detect loops.
    52  	// It is cleared before invoking programs in 'go run', 'go test', 'go generate', and 'go tool'
    53  	// by invoking them in an environment filtered with FilterEnv,
    54  	// so user programs should not see this in their environment.
    55  	countEnv	= "GOTOOLCHAIN_INTERNAL_SWITCH_COUNT"
    56  
    57  	// maxSwitch is the maximum toolchain switching depth.
    58  	// Most uses should never see more than three.
    59  	// (Perhaps one for the initial GOTOOLCHAIN dispatch,
    60  	// a second for go get doing an upgrade, and a third if
    61  	// for some reason the chosen upgrade version is too small
    62  	// by a little.)
    63  	// When the count reaches maxSwitch - 10, we start logging
    64  	// the switched versions for debugging before crashing with
    65  	// a fatal error upon reaching maxSwitch.
    66  	// That should be enough to see the repetition.
    67  	maxSwitch	= 100
    68  )
    69  
    70  // FilterEnv returns a copy of env with internal GOTOOLCHAIN environment
    71  // variables filtered out.
    72  func FilterEnv(env []string) []string {
    73  	// Note: Don't need to filter out targetEnv because Switch does that.
    74  	var out []string
    75  	for _, e := range env {
    76  		if strings.HasPrefix(e, countEnv+"=") {
    77  			continue
    78  		}
    79  		out = append(out, e)
    80  	}
    81  	return out
    82  }
    83  
    84  // Select invokes a different Go toolchain if directed by
    85  // the GOTOOLCHAIN environment variable or the user's configuration
    86  // or go.mod file.
    87  // It must be called early in startup.
    88  // See https://go.dev/doc/toolchain#select.
    89  func Select() {
    90  	log.SetPrefix("go: ")
    91  	defer log.SetPrefix("")
    92  
    93  	if !modload.WillBeEnabled() {
    94  		return
    95  	}
    96  
    97  	// As a special case, let "go env GOTOOLCHAIN" and "go env -w GOTOOLCHAIN=..."
    98  	// be handled by the local toolchain, since an older toolchain may not understand it.
    99  	// This provides an easy way out of "go env -w GOTOOLCHAIN=go1.19" and makes
   100  	// sure that "go env GOTOOLCHAIN" always prints the local go command's interpretation of it.
   101  	// We look for these specific command lines in order to avoid mishandling
   102  	//
   103  	//	GOTOOLCHAIN=go1.999 go env -newflag GOTOOLCHAIN
   104  	//
   105  	// where -newflag is a flag known to Go 1.999 but not known to us.
   106  	if (len(os.Args) == 3 && os.Args[1] == "env" && os.Args[2] == "GOTOOLCHAIN") ||
   107  		(len(os.Args) == 4 && os.Args[1] == "env" && os.Args[2] == "-w" && strings.HasPrefix(os.Args[3], "GOTOOLCHAIN=")) {
   108  		return
   109  	}
   110  
   111  	// Interpret GOTOOLCHAIN to select the Go toolchain to run.
   112  	gotoolchain := cfg.Getenv("GOTOOLCHAIN")
   113  	gover.Startup.GOTOOLCHAIN = gotoolchain
   114  	if gotoolchain == "" {
   115  		// cfg.Getenv should fall back to $GOROOT/go.env,
   116  		// so this should not happen, unless a packager
   117  		// has deleted the GOTOOLCHAIN line from go.env.
   118  		// It can also happen if GOROOT is missing or broken,
   119  		// in which case best to let the go command keep running
   120  		// and diagnose the problem.
   121  		return
   122  	}
   123  
   124  	// Note: minToolchain is what https://go.dev/doc/toolchain#select calls the default toolchain.
   125  	minToolchain := gover.LocalToolchain()
   126  	minVers := gover.Local()
   127  	var mode string
   128  	if gotoolchain == "auto" {
   129  		mode = "auto"
   130  	} else if gotoolchain == "path" {
   131  		mode = "path"
   132  	} else {
   133  		min, suffix, plus := strings.Cut(gotoolchain, "+")	// go1.2.3+auto
   134  		if min != "local" {
   135  			v := gover.FromToolchain(min)
   136  			if v == "" {
   137  				if plus {
   138  					base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", gotoolchain, min)
   139  				}
   140  				base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain)
   141  			}
   142  			minToolchain = min
   143  			minVers = v
   144  		}
   145  		if plus && suffix != "auto" && suffix != "path" {
   146  			base.Fatalf("invalid GOTOOLCHAIN %q: only version suffixes are +auto and +path", gotoolchain)
   147  		}
   148  		mode = suffix
   149  	}
   150  
   151  	gotoolchain = minToolchain
   152  	if (mode == "auto" || mode == "path") && !goInstallVersion() {
   153  		// Read go.mod to find new minimum and suggested toolchain.
   154  		file, goVers, toolchain := modGoToolchain()
   155  		gover.Startup.AutoFile = file
   156  		if toolchain == "default" {
   157  			// "default" means always use the default toolchain,
   158  			// which is already set, so nothing to do here.
   159  			// Note that if we have Go 1.21 installed originally,
   160  			// GOTOOLCHAIN=go1.30.0+auto or GOTOOLCHAIN=go1.30.0,
   161  			// and the go.mod  says "toolchain default", we use Go 1.30, not Go 1.21.
   162  			// That is, default overrides the "auto" part of the calculation
   163  			// but not the minimum that the user has set.
   164  			// Of course, if the go.mod also says "go 1.35", using Go 1.30
   165  			// will provoke an error about the toolchain being too old.
   166  			// That's what people who use toolchain default want:
   167  			// only ever use the toolchain configured by the user
   168  			// (including its environment and go env -w file).
   169  			gover.Startup.AutoToolchain = toolchain
   170  		} else {
   171  			if toolchain != "" {
   172  				// Accept toolchain only if it is > our min.
   173  				// (If it is equal, then min satisfies it anyway: that can matter if min
   174  				// has a suffix like "go1.21.1-foo" and toolchain is "go1.21.1".)
   175  				toolVers := gover.FromToolchain(toolchain)
   176  				if toolVers == "" || (!strings.HasPrefix(toolchain, "go") && !strings.Contains(toolchain, "-go")) {
   177  					base.Fatalf("invalid toolchain %q in %s", toolchain, base.ShortPath(file))
   178  				}
   179  				if gover.Compare(toolVers, minVers) > 0 {
   180  					gotoolchain = toolchain
   181  					minVers = toolVers
   182  					gover.Startup.AutoToolchain = toolchain
   183  				}
   184  			}
   185  			if gover.Compare(goVers, minVers) > 0 {
   186  				gotoolchain = "go" + goVers
   187  				gover.Startup.AutoGoVersion = goVers
   188  				gover.Startup.AutoToolchain = ""	// in case we are overriding it for being too old
   189  			}
   190  		}
   191  	}
   192  
   193  	// If we are invoked as a target toolchain, confirm that
   194  	// we provide the expected version and then run.
   195  	// This check is delayed until after the handling of auto and path
   196  	// so that we have initialized gover.Startup for use in error messages.
   197  	if target := os.Getenv(targetEnv); target != "" && TestVersionSwitch != "loop" {
   198  		if gover.LocalToolchain() != target {
   199  			base.Fatalf("toolchain %v invoked to provide %v", gover.LocalToolchain(), target)
   200  		}
   201  		os.Unsetenv(targetEnv)
   202  
   203  		// Note: It is tempting to check that if gotoolchain != "local"
   204  		// then target == gotoolchain here, as a sanity check that
   205  		// the child has made the same version determination as the parent.
   206  		// This turns out not always to be the case. Specifically, if we are
   207  		// running Go 1.21 with GOTOOLCHAIN=go1.22+auto, which invokes
   208  		// Go 1.22, then 'go get go@1.23.0' or 'go get needs_go_1_23'
   209  		// will invoke Go 1.23, but as the Go 1.23 child the reason for that
   210  		// will not be apparent here: it will look like we should be using Go 1.22.
   211  		// We rely on the targetEnv being set to know not to downgrade.
   212  		// A longer term problem with the sanity check is that the exact details
   213  		// may change over time: there may be other reasons that a future Go
   214  		// version might invoke an older one, and the older one won't know why.
   215  		// Best to just accept that we were invoked to provide a specific toolchain
   216  		// (which we just checked) and leave it at that.
   217  		return
   218  	}
   219  
   220  	if gotoolchain == "local" || gotoolchain == gover.LocalToolchain() {
   221  		// Let the current binary handle the command.
   222  		return
   223  	}
   224  
   225  	// Minimal sanity check of GOTOOLCHAIN setting before search.
   226  	// We want to allow things like go1.20.3 but also gccgo-go1.20.3.
   227  	// We want to disallow mistakes / bad ideas like GOTOOLCHAIN=bash,
   228  	// since we will find that in the path lookup.
   229  	if !strings.HasPrefix(gotoolchain, "go1") && !strings.Contains(gotoolchain, "-go1") {
   230  		base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain)
   231  	}
   232  
   233  	Exec(gotoolchain)
   234  }
   235  
   236  // TestVersionSwitch is set in the test go binary to the value in $TESTGO_VERSION_SWITCH.
   237  // Valid settings are:
   238  //
   239  //	"switch" - simulate version switches by reinvoking the test go binary with a different TESTGO_VERSION.
   240  //	"mismatch" - like "switch" but forget to set TESTGO_VERSION, so it looks like we invoked a mismatched toolchain
   241  //	"loop" - like "mismatch" but forget the target check, causing a toolchain switching loop
   242  var TestVersionSwitch string
   243  
   244  // Exec invokes the specified Go toolchain or else prints an error and exits the process.
   245  // If $GOTOOLCHAIN is set to path or min+path, Exec only considers the PATH
   246  // as a source of Go toolchains. Otherwise Exec tries the PATH but then downloads
   247  // a toolchain if necessary.
   248  func Exec(gotoolchain string) {
   249  	log.SetPrefix("go: ")
   250  
   251  	writeBits = sysWriteBits()
   252  
   253  	count, _ := strconv.Atoi(os.Getenv(countEnv))
   254  	if count >= maxSwitch-10 {
   255  		fmt.Fprintf(os.Stderr, "go: switching from go%v to %v [depth %d]\n", gover.Local(), gotoolchain, count)
   256  	}
   257  	if count >= maxSwitch {
   258  		base.Fatalf("too many toolchain switches")
   259  	}
   260  	os.Setenv(countEnv, fmt.Sprint(count+1))
   261  
   262  	env := cfg.Getenv("GOTOOLCHAIN")
   263  	pathOnly := env == "path" || strings.HasSuffix(env, "+path")
   264  
   265  	// For testing, if TESTGO_VERSION is already in use
   266  	// (only happens in the cmd/go test binary)
   267  	// and TESTGO_VERSION_SWITCH=switch is set,
   268  	// "switch" toolchains by changing TESTGO_VERSION
   269  	// and reinvoking the current binary.
   270  	// The special cases =loop and =mismatch skip the
   271  	// setting of TESTGO_VERSION so that it looks like we
   272  	// accidentally invoked the wrong toolchain,
   273  	// to test detection of that failure mode.
   274  	switch TestVersionSwitch {
   275  	case "switch":
   276  		os.Setenv("TESTGO_VERSION", gotoolchain)
   277  		fallthrough
   278  	case "loop", "mismatch":
   279  		exe, err := os.Executable()
   280  		if err != nil {
   281  			base.Fatalf("%v", err)
   282  		}
   283  		execGoToolchain(gotoolchain, os.Getenv("GOROOT"), exe)
   284  	}
   285  
   286  	// Look in PATH for the toolchain before we download one.
   287  	// This allows custom toolchains as well as reuse of toolchains
   288  	// already installed using go install golang.org/dl/go1.2.3@latest.
   289  	if exe, err := cfg.LookPath(gotoolchain); err == nil {
   290  		execGoToolchain(gotoolchain, "", exe)
   291  	}
   292  
   293  	// GOTOOLCHAIN=auto looks in PATH and then falls back to download.
   294  	// GOTOOLCHAIN=path only looks in PATH.
   295  	if pathOnly {
   296  		base.Fatalf("cannot find %q in PATH", gotoolchain)
   297  	}
   298  
   299  	// Set up modules without an explicit go.mod, to download distribution.
   300  	modload.Reset()
   301  	modload.ForceUseModules = true
   302  	modload.RootMode = modload.NoRoot
   303  	modload.Init()
   304  
   305  	// Download and unpack toolchain module into module cache.
   306  	// Note that multiple go commands might be doing this at the same time,
   307  	// and that's OK: the module cache handles that case correctly.
   308  	m := module.Version{
   309  		Path:		gotoolchainModule,
   310  		Version:	gotoolchainVersion + "-" + gotoolchain + "." + runtime.GOOS + "-" + runtime.GOARCH,
   311  	}
   312  	dir, err := modfetch.Download(context.Background(), m)
   313  	if err != nil {
   314  		if errors.Is(err, fs.ErrNotExist) {
   315  			base.Fatalf("download %s for %s/%s: toolchain not available", gotoolchain, runtime.GOOS, runtime.GOARCH)
   316  		}
   317  		base.Fatalf("download %s: %v", gotoolchain, err)
   318  	}
   319  
   320  	// On first use after download, set the execute bits on the commands
   321  	// so that we can run them. Note that multiple go commands might be
   322  	// doing this at the same time, but if so no harm done.
   323  	if runtime.GOOS != "windows" {
   324  		info, err := os.Stat(filepath.Join(dir, "bin/go"))
   325  		if err != nil {
   326  			base.Fatalf("download %s: %v", gotoolchain, err)
   327  		}
   328  		if info.Mode()&0111 == 0 {
   329  			// allowExec sets the exec permission bits on all files found in dir.
   330  			allowExec := func(dir string) {
   331  				err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
   332  					if err != nil {
   333  						return err
   334  					}
   335  					if !d.IsDir() {
   336  						info, err := os.Stat(path)
   337  						if err != nil {
   338  							return err
   339  						}
   340  						if err := os.Chmod(path, info.Mode()&0777|0111); err != nil {
   341  							return err
   342  						}
   343  					}
   344  					return nil
   345  				})
   346  				if err != nil {
   347  					base.Fatalf("download %s: %v", gotoolchain, err)
   348  				}
   349  			}
   350  
   351  			// Set the bits in pkg/tool before bin/go.
   352  			// If we are racing with another go command and do bin/go first,
   353  			// then the check of bin/go above might succeed, the other go command
   354  			// would skip its own mode-setting, and then the go command might
   355  			// try to run a tool before we get to setting the bits on pkg/tool.
   356  			// Setting pkg/tool before bin/go avoids that ordering problem.
   357  			// The only other tool the go command invokes is gofmt,
   358  			// so we set that one explicitly before handling bin (which will include bin/go).
   359  			allowExec(filepath.Join(dir, "pkg/tool"))
   360  			allowExec(filepath.Join(dir, "bin/gofmt"))
   361  			allowExec(filepath.Join(dir, "bin"))
   362  		}
   363  	}
   364  
   365  	srcUGoMod := filepath.Join(dir, "src/_go.mod")
   366  	srcGoMod := filepath.Join(dir, "src/go.mod")
   367  	if size(srcGoMod) != size(srcUGoMod) {
   368  		err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
   369  			if err != nil {
   370  				return err
   371  			}
   372  			if path == srcUGoMod {
   373  				// Leave for last, in case we are racing with another go command.
   374  				return nil
   375  			}
   376  			if pdir, name := filepath.Split(path); name == "_go.mod" {
   377  				if err := raceSafeCopy(path, pdir+"go.mod"); err != nil {
   378  					return err
   379  				}
   380  			}
   381  			return nil
   382  		})
   383  		// Handle src/go.mod; this is the signal to other racing go commands
   384  		// that everything is okay and they can skip this step.
   385  		if err == nil {
   386  			err = raceSafeCopy(srcUGoMod, srcGoMod)
   387  		}
   388  		if err != nil {
   389  			base.Fatalf("download %s: %v", gotoolchain, err)
   390  		}
   391  	}
   392  
   393  	// Reinvoke the go command.
   394  	execGoToolchain(gotoolchain, dir, filepath.Join(dir, "bin/go"))
   395  }
   396  
   397  func size(path string) int64 {
   398  	info, err := os.Stat(path)
   399  	if err != nil {
   400  		return -1
   401  	}
   402  	return info.Size()
   403  }
   404  
   405  var writeBits fs.FileMode
   406  
   407  // raceSafeCopy copies the file old to the file new, being careful to ensure
   408  // that if multiple go commands call raceSafeCopy(old, new) at the same time,
   409  // they don't interfere with each other: both will succeed and return and
   410  // later observe the correct content in new. Like in the build cache, we arrange
   411  // this by opening new without truncation and then writing the content.
   412  // Both go commands can do this simultaneously and will write the same thing
   413  // (old never changes content).
   414  func raceSafeCopy(old, new string) error {
   415  	oldInfo, err := os.Stat(old)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	newInfo, err := os.Stat(new)
   420  	if err == nil && newInfo.Size() == oldInfo.Size() {
   421  		return nil
   422  	}
   423  	data, err := os.ReadFile(old)
   424  	if err != nil {
   425  		return err
   426  	}
   427  	// The module cache has unwritable directories by default.
   428  	// Restore the user write bit in the directory so we can create
   429  	// the new go.mod file. We clear it again at the end on a
   430  	// best-effort basis (ignoring failures).
   431  	dir := filepath.Dir(old)
   432  	info, err := os.Stat(dir)
   433  	if err != nil {
   434  		return err
   435  	}
   436  	if err := os.Chmod(dir, info.Mode()|writeBits); err != nil {
   437  		return err
   438  	}
   439  	defer os.Chmod(dir, info.Mode())
   440  	// Note: create the file writable, so that a racing go command
   441  	// doesn't get an error before we store the actual data.
   442  	f, err := os.OpenFile(new, os.O_CREATE|os.O_WRONLY, writeBits&^0o111)
   443  	if err != nil {
   444  		// If OpenFile failed because a racing go command completed our work
   445  		// (and then OpenFile failed because the directory or file is now read-only),
   446  		// count that as a success.
   447  		if size(old) == size(new) {
   448  			return nil
   449  		}
   450  		return err
   451  	}
   452  	defer os.Chmod(new, oldInfo.Mode())
   453  	if _, err := f.Write(data); err != nil {
   454  		f.Close()
   455  		return err
   456  	}
   457  	return f.Close()
   458  }
   459  
   460  // modGoToolchain finds the enclosing go.work or go.mod file
   461  // and returns the go version and toolchain lines from the file.
   462  // The toolchain line overrides the version line
   463  func modGoToolchain() (file, goVers, toolchain string) {
   464  	wd := base.UncachedCwd()
   465  	file = modload.FindGoWork(wd)
   466  	// $GOWORK can be set to a file that does not yet exist, if we are running 'go work init'.
   467  	// Do not try to load the file in that case
   468  	if _, err := os.Stat(file); err != nil {
   469  		file = ""
   470  	}
   471  	if file == "" {
   472  		file = modload.FindGoMod(wd)
   473  	}
   474  	if file == "" {
   475  		return "", "", ""
   476  	}
   477  
   478  	data, err := os.ReadFile(file)
   479  	if err != nil {
   480  		base.Fatalf("%v", err)
   481  	}
   482  	return file, gover.GoModLookup(data, "go"), gover.GoModLookup(data, "toolchain")
   483  }
   484  
   485  // goInstallVersion reports whether the command line is go install m@v or go run m@v.
   486  // If so, Select must not read the go.mod or go.work file in "auto" or "path" mode.
   487  func goInstallVersion() bool {
   488  	// Note: We assume there are no flags between 'go' and 'install' or 'run'.
   489  	// During testing there are some debugging flags that are accepted
   490  	// in that position, but in production go binaries there are not.
   491  	if len(os.Args) < 3 {
   492  		return false
   493  	}
   494  
   495  	var cmdFlags *flag.FlagSet
   496  	switch os.Args[1] {
   497  	default:
   498  		// Command doesn't support a pkg@version as the main module.
   499  		return false
   500  	case "install":
   501  		cmdFlags = &work.CmdInstall.Flag
   502  	case "run":
   503  		cmdFlags = &run.CmdRun.Flag
   504  	}
   505  
   506  	// The modcachrw flag is unique, in that it affects how we fetch the
   507  	// requested module to even figure out what toolchain it needs.
   508  	// We need to actually set it before we check the toolchain version.
   509  	// (See https://go.dev/issue/64282.)
   510  	modcacherwFlag := cmdFlags.Lookup("modcacherw")
   511  	if modcacherwFlag == nil {
   512  		base.Fatalf("internal error: modcacherw flag not registered for command")
   513  	}
   514  	modcacherwVal, ok := modcacherwFlag.Value.(interface {
   515  		IsBoolFlag() bool
   516  		flag.Value
   517  	})
   518  	if !ok || !modcacherwVal.IsBoolFlag() {
   519  		base.Fatalf("internal error: modcacherw is not a boolean flag")
   520  	}
   521  
   522  	// Make a best effort to parse the command's args to find the pkg@version
   523  	// argument and the -modcacherw flag.
   524  	var (
   525  		pkgArg		string
   526  		modcacherwSeen	bool
   527  	)
   528  	for args := os.Args[2:]; len(args) > 0; {
   529  		a := args[0]
   530  		args = args[1:]
   531  		if a == "--" {
   532  			if len(args) == 0 {
   533  				return false
   534  			}
   535  			pkgArg = args[0]
   536  			break
   537  		}
   538  
   539  		a, ok := strings.CutPrefix(a, "-")
   540  		if !ok {
   541  			// Not a flag argument. Must be a package.
   542  			pkgArg = a
   543  			break
   544  		}
   545  		a = strings.TrimPrefix(a, "-")	// Treat --flag as -flag.
   546  
   547  		name, val, hasEq := strings.Cut(a, "=")
   548  
   549  		if name == "modcacherw" {
   550  			if !hasEq {
   551  				val = "true"
   552  			}
   553  			if err := modcacherwVal.Set(val); err != nil {
   554  				return false
   555  			}
   556  			modcacherwSeen = true
   557  			continue
   558  		}
   559  
   560  		if hasEq {
   561  			// Already has a value; don't bother parsing it.
   562  			continue
   563  		}
   564  
   565  		f := run.CmdRun.Flag.Lookup(a)
   566  		if f == nil {
   567  			// We don't know whether this flag is a boolean.
   568  			if os.Args[1] == "run" {
   569  				// We don't know where to find the pkg@version argument.
   570  				// For run, the pkg@version can be anywhere on the command line,
   571  				// because it is preceded by run flags and followed by arguments to the
   572  				// program being run. Since we don't know whether this flag takes
   573  				// an argument, we can't reliably identify the end of the run flags.
   574  				// Just give up and let the user clarify using the "=" form..
   575  				return false
   576  			}
   577  
   578  			// We would like to let 'go install -newflag pkg@version' work even
   579  			// across a toolchain switch. To make that work, assume by default that
   580  			// the pkg@version is the last argument and skip the remaining args unless
   581  			// we spot a plausible "-modcacherw" flag.
   582  			for len(args) > 0 {
   583  				a := args[0]
   584  				name, _, _ := strings.Cut(a, "=")
   585  				if name == "-modcacherw" || name == "--modcacherw" {
   586  					break
   587  				}
   588  				if len(args) == 1 && !strings.HasPrefix(a, "-") {
   589  					pkgArg = a
   590  				}
   591  				args = args[1:]
   592  			}
   593  			continue
   594  		}
   595  
   596  		if bf, ok := f.Value.(interface{ IsBoolFlag() bool }); !ok || !bf.IsBoolFlag() {
   597  			// The next arg is the value for this flag. Skip it.
   598  			args = args[1:]
   599  			continue
   600  		}
   601  	}
   602  
   603  	if !strings.Contains(pkgArg, "@") || build.IsLocalImport(pkgArg) || filepath.IsAbs(pkgArg) {
   604  		return false
   605  	}
   606  	path, version, _ := strings.Cut(pkgArg, "@")
   607  	if path == "" || version == "" || gover.IsToolchain(path) {
   608  		return false
   609  	}
   610  
   611  	if !modcacherwSeen && base.InGOFLAGS("-modcacherw") {
   612  		fs := flag.NewFlagSet("goInstallVersion", flag.ExitOnError)
   613  		fs.Var(modcacherwVal, "modcacherw", modcacherwFlag.Usage)
   614  		base.SetFromGOFLAGS(fs)
   615  	}
   616  
   617  	// It would be correct to simply return true here, bypassing use
   618  	// of the current go.mod or go.work, and let "go run" or "go install"
   619  	// do the rest, including a toolchain switch.
   620  	// Our goal instead is, since we have gone to the trouble of handling
   621  	// unknown flags to some degree, to run the switch now, so that
   622  	// these commands can switch to a newer toolchain directed by the
   623  	// go.mod which may actually understand the flag.
   624  	// This was brought up during the go.dev/issue/57001 proposal discussion
   625  	// and may end up being common in self-contained "go install" or "go run"
   626  	// command lines if we add new flags in the future.
   627  
   628  	// Set up modules without an explicit go.mod, to download go.mod.
   629  	modload.ForceUseModules = true
   630  	modload.RootMode = modload.NoRoot
   631  	modload.Init()
   632  	defer modload.Reset()
   633  
   634  	// See internal/load.PackagesAndErrorsOutsideModule
   635  	ctx := context.Background()
   636  	allowed := modload.CheckAllowed
   637  	if modload.IsRevisionQuery(path, version) {
   638  		// Don't check for retractions if a specific revision is requested.
   639  		allowed = nil
   640  	}
   641  	noneSelected := func(path string) (version string) { return "none" }
   642  	_, err := modload.QueryPackages(ctx, path, version, noneSelected, allowed)
   643  	if errors.Is(err, gover.ErrTooNew) {
   644  		// Run early switch, same one go install or go run would eventually do,
   645  		// if it understood all the command-line flags.
   646  		SwitchOrFatal(ctx, err)
   647  	}
   648  
   649  	return true	// pkg@version found
   650  }