Source file src/cmd/go/internal/toolchain/select.go

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

View as plain text