| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package toolchain implements dynamic switching of Go toolchains. |
| package toolchain |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "go/build" |
| "io/fs" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strconv" |
| "strings" |
| |
| "cmd/go/internal/base" |
| "cmd/go/internal/cfg" |
| "cmd/go/internal/gover" |
| "cmd/go/internal/modfetch" |
| "cmd/go/internal/modload" |
| "cmd/go/internal/run" |
| |
| "golang.org/x/mod/module" |
| ) |
| |
| const ( |
| // We download golang.org/toolchain version v0.0.1-<gotoolchain>.<goos>-<goarch>. |
| // If the 0.0.1 indicates anything at all, its the version of the toolchain packaging: |
| // if for some reason we needed to change the way toolchains are packaged into |
| // module zip files in a future version of Go, we could switch to v0.0.2 and then |
| // older versions expecting the old format could use v0.0.1 and newer versions |
| // would use v0.0.2. Of course, then we'd also have to publish two of each |
| // module zip file. It's not likely we'll ever need to change this. |
| gotoolchainModule = "golang.org/toolchain" |
| gotoolchainVersion = "v0.0.1" |
| |
| // targetEnv is a special environment variable set to the expected |
| // toolchain version during the toolchain switch by the parent |
| // process and cleared in the child process. When set, that indicates |
| // to the child to confirm that it provides the expected toolchain version. |
| targetEnv = "GOTOOLCHAIN_INTERNAL_SWITCH_VERSION" |
| |
| // countEnv is a special environment variable |
| // that is incremented during each toolchain switch, to detect loops. |
| // It is cleared before invoking programs in 'go run', 'go test', 'go generate', and 'go tool' |
| // by invoking them in an environment filtered with FilterEnv, |
| // so user programs should not see this in their environment. |
| countEnv = "GOTOOLCHAIN_INTERNAL_SWITCH_COUNT" |
| |
| // maxSwitch is the maximum toolchain switching depth. |
| // Most uses should never see more than three. |
| // (Perhaps one for the initial GOTOOLCHAIN dispatch, |
| // a second for go get doing an upgrade, and a third if |
| // for some reason the chosen upgrade version is too small |
| // by a little.) |
| // When the count reaches maxSwitch - 10, we start logging |
| // the switched versions for debugging before crashing with |
| // a fatal error upon reaching maxSwitch. |
| // That should be enough to see the repetition. |
| maxSwitch = 100 |
| ) |
| |
| // FilterEnv returns a copy of env with internal GOTOOLCHAIN environment |
| // variables filtered out. |
| func FilterEnv(env []string) []string { |
| // Note: Don't need to filter out targetEnv because Switch does that. |
| var out []string |
| for _, e := range env { |
| if strings.HasPrefix(e, countEnv+"=") { |
| continue |
| } |
| out = append(out, e) |
| } |
| return out |
| } |
| |
| // Select invokes a different Go toolchain if directed by |
| // the GOTOOLCHAIN environment variable or the user's configuration |
| // or go.mod file. |
| // 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 |
| } |
| |
| // As a special case, let "go env GOTOOLCHAIN" and "go env -w GOTOOLCHAIN=..." |
| // be handled by the local toolchain, since an older toolchain may not understand it. |
| // This provides an easy way out of "go env -w GOTOOLCHAIN=go1.19" and makes |
| // sure that "go env GOTOOLCHAIN" always prints the local go command's interpretation of it. |
| // We look for these specific command lines in order to avoid mishandling |
| // |
| // GOTOOLCHAIN=go1.999 go env -newflag GOTOOLCHAIN |
| // |
| // where -newflag is a flag known to Go 1.999 but not known to us. |
| if (len(os.Args) == 3 && os.Args[1] == "env" && os.Args[2] == "GOTOOLCHAIN") || |
| (len(os.Args) == 4 && os.Args[1] == "env" && os.Args[2] == "-w" && strings.HasPrefix(os.Args[3], "GOTOOLCHAIN=")) { |
| return |
| } |
| |
| // Interpret GOTOOLCHAIN to select the Go toolchain to run. |
| gotoolchain := cfg.Getenv("GOTOOLCHAIN") |
| gover.Startup.GOTOOLCHAIN = gotoolchain |
| if gotoolchain == "" { |
| // cfg.Getenv should fall back to $GOROOT/go.env, |
| // so this should not happen, unless a packager |
| // has deleted the GOTOOLCHAIN line from go.env. |
| // It can also happen if GOROOT is missing or broken, |
| // in which case best to let the go command keep running |
| // and diagnose the problem. |
| return |
| } |
| |
| // Note: minToolchain is what https://go.dev/doc/toolchain#select calls the default toolchain. |
| minToolchain := gover.LocalToolchain() |
| minVers := gover.Local() |
| var mode string |
| if gotoolchain == "auto" { |
| mode = "auto" |
| } else if gotoolchain == "path" { |
| mode = "path" |
| } else { |
| min, suffix, plus := strings.Cut(gotoolchain, "+") // go1.2.3+auto |
| if min != "local" { |
| v := gover.FromToolchain(min) |
| if v == "" { |
| if plus { |
| base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", gotoolchain, min) |
| } |
| base.Fatalf("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) |
| } |
| mode = suffix |
| } |
| |
| gotoolchain = minToolchain |
| if (mode == "auto" || mode == "path") && !goInstallVersion() { |
| // Read go.mod to find new minimum and suggested toolchain. |
| file, goVers, toolchain := modGoToolchain() |
| gover.Startup.AutoFile = file |
| if toolchain == "default" { |
| // "default" means always use the default toolchain, |
| // which is already set, so nothing to do here. |
| // Note that if we have Go 1.21 installed originally, |
| // GOTOOLCHAIN=go1.30.0+auto or GOTOOLCHAIN=go1.30.0, |
| // and the go.mod says "toolchain default", we use Go 1.30, not Go 1.21. |
| // That is, default overrides the "auto" part of the calculation |
| // but not the minimum that the user has set. |
| // Of course, if the go.mod also says "go 1.35", using Go 1.30 |
| // will provoke an error about the toolchain being too old. |
| // That's what people who use toolchain default want: |
| // only ever use the toolchain configured by the user |
| // (including its environment and go env -w file). |
| gover.Startup.AutoToolchain = toolchain |
| } else { |
| if toolchain != "" { |
| // Accept toolchain only if it is >= our min. |
| 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)) |
| } |
| if gover.Compare(toolVers, minVers) >= 0 { |
| gotoolchain = toolchain |
| minVers = toolVers |
| gover.Startup.AutoToolchain = toolchain |
| } |
| } |
| if gover.Compare(goVers, minVers) > 0 { |
| gotoolchain = "go" + goVers |
| gover.Startup.AutoGoVersion = goVers |
| gover.Startup.AutoToolchain = "" // in case we are overriding it for being too old |
| } |
| } |
| } |
| |
| // If we are invoked as a target toolchain, confirm that |
| // we provide the expected version and then run. |
| // This check is delayed until after the handling of auto and path |
| // 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) |
| } |
| os.Unsetenv(targetEnv) |
| |
| // Note: It is tempting to check that if gotoolchain != "local" |
| // then target == gotoolchain here, as a sanity check that |
| // the child has made the same version determination as the parent. |
| // This turns out not always to be the case. Specifically, if we are |
| // running Go 1.21 with GOTOOLCHAIN=go1.22+auto, which invokes |
| // Go 1.22, then 'go get go@1.23.0' or 'go get needs_go_1_23' |
| // will invoke Go 1.23, but as the Go 1.23 child the reason for that |
| // will not be apparent here: it will look like we should be using Go 1.22. |
| // We rely on the targetEnv being set to know not to downgrade. |
| // A longer term problem with the sanity check is that the exact details |
| // may change over time: there may be other reasons that a future Go |
| // version might invoke an older one, and the older one won't know why. |
| // Best to just accept that we were invoked to provide a specific toolchain |
| // (which we just checked) and leave it at that. |
| return |
| } |
| |
| if gotoolchain == "local" || gotoolchain == gover.LocalToolchain() { |
| // Let the current binary handle the command. |
| return |
| } |
| |
| // Minimal sanity check of GOTOOLCHAIN setting before search. |
| // We want to allow things like go1.20.3 but also gccgo-go1.20.3. |
| // 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) |
| } |
| |
| Exec(gotoolchain) |
| } |
| |
| // TestVersionSwitch is set in the test go binary to the value in $TESTGO_VERSION_SWITCH. |
| // Valid settings are: |
| // |
| // "switch" - simulate version switches by reinvoking the test go binary with a different TESTGO_VERSION. |
| // "mismatch" - like "switch" but forget to set TESTGO_VERSION, so it looks like we invoked a mismatched toolchain |
| // "loop" - like "mismatch" but forget the target check, causing a toolchain switching loop |
| var TestVersionSwitch string |
| |
| // Exec invokes the specified Go toolchain or else prints an error and exits the process. |
| // If $GOTOOLCHAIN is set to path or min+path, Exec only considers the PATH |
| // 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") |
| } |
| os.Setenv(countEnv, fmt.Sprint(count+1)) |
| |
| env := cfg.Getenv("GOTOOLCHAIN") |
| pathOnly := env == "path" || strings.HasSuffix(env, "+path") |
| |
| // For testing, if TESTGO_VERSION is already in use |
| // (only happens in the cmd/go test binary) |
| // and TESTGO_VERSION_SWITCH=switch is set, |
| // "switch" toolchains by changing TESTGO_VERSION |
| // and reinvoking the current binary. |
| // The special cases =loop and =mismatch skip the |
| // setting of TESTGO_VERSION so that it looks like we |
| // accidentally invoked the wrong toolchain, |
| // to test detection of that failure mode. |
| switch TestVersionSwitch { |
| case "switch": |
| os.Setenv("TESTGO_VERSION", gotoolchain) |
| fallthrough |
| case "loop", "mismatch": |
| exe, err := os.Executable() |
| if err != nil { |
| base.Fatalf("%v", err) |
| } |
| execGoToolchain(gotoolchain, os.Getenv("GOROOT"), exe) |
| } |
| |
| // Look in PATH for the toolchain before we download one. |
| // This allows custom toolchains as well as reuse of toolchains |
| // already installed using go install golang.org/dl/go1.2.3@latest. |
| if exe, err := exec.LookPath(gotoolchain); err == nil { |
| execGoToolchain(gotoolchain, "", exe) |
| } |
| |
| // 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) |
| } |
| |
| // Set up modules without an explicit go.mod, to download distribution. |
| modload.Reset() |
| modload.ForceUseModules = true |
| modload.RootMode = modload.NoRoot |
| modload.Init() |
| |
| // Download and unpack toolchain module into module cache. |
| // Note that multiple go commands might be doing this at the same time, |
| // and that's OK: the module cache handles that case correctly. |
| m := module.Version{ |
| Path: gotoolchainModule, |
| Version: gotoolchainVersion + "-" + gotoolchain + "." + runtime.GOOS + "-" + runtime.GOARCH, |
| } |
| 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("download %s: %v", gotoolchain, err) |
| } |
| |
| // On first use after download, set the execute bits on the commands |
| // so that we can run them. Note that multiple go commands might be |
| // doing this at the same time, but if so no harm done. |
| if runtime.GOOS != "windows" { |
| info, err := os.Stat(filepath.Join(dir, "bin/go")) |
| if err != nil { |
| base.Fatalf("download %s: %v", gotoolchain, err) |
| } |
| if info.Mode()&0111 == 0 { |
| // allowExec sets the exec permission bits on all files found in dir. |
| allowExec := func(dir string) { |
| err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { |
| if err != nil { |
| return err |
| } |
| if !d.IsDir() { |
| info, err := os.Stat(path) |
| if err != nil { |
| return err |
| } |
| if err := os.Chmod(path, info.Mode()&0777|0111); err != nil { |
| return err |
| } |
| } |
| return nil |
| }) |
| if err != nil { |
| base.Fatalf("download %s: %v", gotoolchain, err) |
| } |
| } |
| |
| // Set the bits in pkg/tool before bin/go. |
| // If we are racing with another go command and do bin/go first, |
| // then the check of bin/go above might succeed, the other go command |
| // would skip its own mode-setting, and then the go command might |
| // try to run a tool before we get to setting the bits on pkg/tool. |
| // Setting pkg/tool before bin/go avoids that ordering problem. |
| // The only other tool the go command invokes is gofmt, |
| // so we set that one explicitly before handling bin (which will include bin/go). |
| allowExec(filepath.Join(dir, "pkg/tool")) |
| allowExec(filepath.Join(dir, "bin/gofmt")) |
| allowExec(filepath.Join(dir, "bin")) |
| } |
| } |
| |
| srcUGoMod := filepath.Join(dir, "src/_go.mod") |
| srcGoMod := filepath.Join(dir, "src/go.mod") |
| if size(srcGoMod) != size(srcUGoMod) { |
| err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { |
| if err != nil { |
| return err |
| } |
| if path == srcUGoMod { |
| // Leave for last, in case we are racing with another go command. |
| return nil |
| } |
| if pdir, name := filepath.Split(path); name == "_go.mod" { |
| if err := raceSafeCopy(path, pdir+"go.mod"); err != nil { |
| return err |
| } |
| } |
| return nil |
| }) |
| // Handle src/go.mod; this is the signal to other racing go commands |
| // that everything is okay and they can skip this step. |
| if err == nil { |
| err = raceSafeCopy(srcUGoMod, srcGoMod) |
| } |
| if err != nil { |
| base.Fatalf("download %s: %v", gotoolchain, err) |
| } |
| } |
| |
| // Reinvoke the go command. |
| execGoToolchain(gotoolchain, dir, filepath.Join(dir, "bin/go")) |
| } |
| |
| func size(path string) int64 { |
| info, err := os.Stat(path) |
| if err != nil { |
| return -1 |
| } |
| return info.Size() |
| } |
| |
| var writeBits fs.FileMode |
| |
| // raceSafeCopy copies the file old to the file new, being careful to ensure |
| // that if multiple go commands call raceSafeCopy(old, new) at the same time, |
| // they don't interfere with each other: both will succeed and return and |
| // later observe the correct content in new. Like in the build cache, we arrange |
| // this by opening new without truncation and then writing the content. |
| // Both go commands can do this simultaneously and will write the same thing |
| // (old never changes content). |
| func raceSafeCopy(old, new string) error { |
| oldInfo, err := os.Stat(old) |
| if err != nil { |
| return err |
| } |
| newInfo, err := os.Stat(new) |
| if err == nil && newInfo.Size() == oldInfo.Size() { |
| return nil |
| } |
| data, err := os.ReadFile(old) |
| if err != nil { |
| return err |
| } |
| // The module cache has unwritable directories by default. |
| // Restore the user write bit in the directory so we can create |
| // the new go.mod file. We clear it again at the end on a |
| // best-effort basis (ignoring failures). |
| dir := filepath.Dir(old) |
| info, err := os.Stat(dir) |
| if err != nil { |
| return err |
| } |
| if err := os.Chmod(dir, info.Mode()|writeBits); err != nil { |
| return err |
| } |
| defer os.Chmod(dir, info.Mode()) |
| // Note: create the file writable, so that a racing go command |
| // doesn't get an error before we store the actual data. |
| f, err := os.OpenFile(new, os.O_CREATE|os.O_WRONLY, writeBits&^0o111) |
| if err != nil { |
| // If OpenFile failed because a racing go command completed our work |
| // (and then OpenFile failed because the directory or file is now read-only), |
| // count that as a success. |
| if size(old) == size(new) { |
| return nil |
| } |
| return err |
| } |
| defer os.Chmod(new, oldInfo.Mode()) |
| if _, err := f.Write(data); err != nil { |
| f.Close() |
| return err |
| } |
| return f.Close() |
| } |
| |
| // modGoToolchain finds the enclosing go.work or go.mod file |
| // and returns the go version and toolchain lines from the file. |
| // The toolchain line overrides the version line |
| func modGoToolchain() (file, goVers, toolchain string) { |
| wd := base.UncachedCwd() |
| file = modload.FindGoWork(wd) |
| // $GOWORK can be set to a file that does not yet exist, if we are running 'go work init'. |
| // Do not try to load the file in that case |
| if _, err := os.Stat(file); err != nil { |
| file = "" |
| } |
| if file == "" { |
| file = modload.FindGoMod(wd) |
| } |
| if file == "" { |
| return "", "", "" |
| } |
| |
| data, err := os.ReadFile(file) |
| if err != nil { |
| base.Fatalf("%v", err) |
| } |
| return file, gover.GoModLookup(data, "go"), gover.GoModLookup(data, "toolchain") |
| } |
| |
| // goInstallVersion reports whether the command line is go install m@v or go run m@v. |
| // If so, Select must not read the go.mod or go.work file in "auto" or "path" mode. |
| func goInstallVersion() bool { |
| // Note: We assume there are no flags between 'go' and 'install' or 'run'. |
| // During testing there are some debugging flags that are accepted |
| // in that position, but in production go binaries there are not. |
| if len(os.Args) < 3 || (os.Args[1] != "install" && os.Args[1] != "run") { |
| return false |
| } |
| |
| // Check for pkg@version. |
| var arg string |
| switch os.Args[1] { |
| default: |
| return false |
| case "install": |
| // We would like to let 'go install -newflag pkg@version' work even |
| // 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] |
| 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:] |
| for i := 0; i < len(args); i++ { |
| a := args[i] |
| if !strings.HasPrefix(a, "-") { |
| arg = a |
| break |
| } |
| if a == "-" { |
| // non-flag but also non-pkg@version |
| return false |
| } |
| if a == "--" { |
| if i+1 >= len(args) { |
| return false |
| } |
| arg = args[i+1] |
| break |
| } |
| a = strings.TrimPrefix(a, "-") |
| a = strings.TrimPrefix(a, "-") |
| if strings.HasPrefix(a, "-") { |
| // non-flag but also non-pkg@version |
| return false |
| } |
| if strings.Contains(a, "=") { |
| // already has value |
| continue |
| } |
| f := run.CmdRun.Flag.Lookup(a) |
| if f == nil { |
| // Unknown flag. Give up. The command is going to fail in flag parsing. |
| return false |
| } |
| if bf, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && bf.IsBoolFlag() { |
| // Does not take value. |
| continue |
| } |
| i++ // Does take a value; skip it. |
| } |
| } |
| if !strings.Contains(arg, "@") || build.IsLocalImport(arg) || filepath.IsAbs(arg) { |
| return false |
| } |
| path, version, _ := strings.Cut(arg, "@") |
| if path == "" || version == "" || gover.IsToolchain(path) { |
| return false |
| } |
| |
| // 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. |
| // Our goal instead is, since we have gone to the trouble of handling |
| // unknown flags to some degree, to run the switch now, so that |
| // these commands can switch to a newer toolchain directed by the |
| // go.mod which may actually understand the flag. |
| // This was brought up during the go.dev/issue/57001 proposal discussion |
| // and may end up being common in self-contained "go install" or "go run" |
| // command lines if we add new flags in the future. |
| |
| // Set up modules without an explicit go.mod, to download go.mod. |
| modload.ForceUseModules = true |
| modload.RootMode = modload.NoRoot |
| modload.Init() |
| defer modload.Reset() |
| |
| // See internal/load.PackagesAndErrorsOutsideModule |
| ctx := context.Background() |
| allowed := modload.CheckAllowed |
| if modload.IsRevisionQuery(path, version) { |
| // Don't check for retractions if a specific revision is requested. |
| allowed = nil |
| } |
| noneSelected := func(path string) (version string) { return "none" } |
| _, err := modload.QueryPackages(ctx, path, version, noneSelected, allowed) |
| if errors.Is(err, gover.ErrTooNew) { |
| // Run early switch, same one go install or go run would eventually do, |
| // if it understood all the command-line flags. |
| SwitchOrFatal(ctx, err) |
| } |
| |
| return true // pkg@version found |
| } |