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