| // 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. |
| |
| //go:build !js && !wasip1 |
| |
| package main |
| |
| import ( |
| "context" |
| "fmt" |
| "go/build" |
| "internal/godebug" |
| "io/fs" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "syscall" |
| |
| "cmd/go/internal/base" |
| "cmd/go/internal/cfg" |
| "cmd/go/internal/gover" |
| "cmd/go/internal/modcmd" |
| "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" |
| |
| // gotoolchainSwitchEnv is a special environment variable |
| // set to 1 during the toolchain switch by the parent process |
| // and cleared in the child process. When set, that indicates |
| // to the child not to do its own toolchain switch logic, |
| // to avoid an infinite recursion if for some reason a toolchain |
| // did not believe it could handle its own version and then |
| // reinvoked itself. |
| gotoolchainSwitchEnv = "GOTOOLCHAIN_INTERNAL_SWITCH" |
| ) |
| |
| // switchGoToolchain invokes a different Go toolchain if directed by |
| // the GOTOOLCHAIN environment variable or the user's configuration |
| // or go.mod file. |
| func switchGoToolchain() { |
| log.SetPrefix("go: ") |
| defer log.SetPrefix("") |
| |
| sw := os.Getenv(gotoolchainSwitchEnv) |
| os.Unsetenv(gotoolchainSwitchEnv) |
| |
| if !modload.WillBeEnabled() || sw == "1" { |
| return |
| } |
| |
| gotoolchain := cfg.Getenv("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 |
| } |
| gover.Startup.GOTOOLCHAIN = gotoolchain |
| |
| var minToolchain, minVers string |
| if x, y, ok := strings.Cut(gotoolchain, "+"); ok { // go1.2.3+auto |
| orig := gotoolchain |
| minToolchain, gotoolchain = x, y |
| minVers = gover.FromToolchain(minToolchain) |
| if minVers == "" { |
| base.Fatalf("invalid GOTOOLCHAIN %q: invalid minimum toolchain %q", orig, minToolchain) |
| } |
| if gotoolchain != "auto" && gotoolchain != "path" { |
| base.Fatalf("invalid GOTOOLCHAIN %q: only version suffixes are +auto and +path", orig) |
| } |
| } else { |
| minVers = gover.Local() |
| minToolchain = "go" + minVers |
| } |
| |
| pathOnly := gotoolchain == "path" |
| if gotoolchain == "auto" || gotoolchain == "path" { |
| gotoolchain = minToolchain |
| |
| // Locate and read go.mod or go.work. |
| // For go install m@v, it's the installed module's go.mod. |
| if m, goVers, ok := goInstallVersion(); ok { |
| if gover.Compare(goVers, minVers) > 0 { |
| // Always print, because otherwise there's no way for the user to know |
| // that a non-default toolchain version is being used here. |
| // (Normally you can run "go version", but go install m@v ignores the |
| // context that "go version" works in.) |
| fmt.Fprintf(os.Stderr, "go: using go%s for %v\n", goVers, m) |
| gotoolchain = "go" + goVers |
| } |
| } else { |
| file, goVers, toolchain := modGoToolchain() |
| if toolchain == "local" { |
| // Local means always use the default local 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 local", we use Go 1.30, not Go 1.21. |
| // That is, local 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 local want: |
| // only ever use the toolchain configured in the local system |
| // (including its environment and go env -w file). |
| } else if toolchain != "" { |
| // Accept toolchain only if it is >= our min. |
| toolVers := gover.FromToolchain(toolchain) |
| if gover.Compare(toolVers, minVers) >= 0 { |
| gotoolchain = toolchain |
| } |
| } else { |
| if gover.Compare(goVers, minVers) > 0 { |
| gotoolchain = "go" + goVers |
| } |
| } |
| gover.Startup.AutoFile = file |
| gover.Startup.AutoGoVersion = goVers |
| gover.Startup.AutoToolchain = toolchain |
| } |
| } |
| |
| if gotoolchain == "local" || gotoolchain == "go"+gover.Local() { |
| // 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. |
| // gover.FromToolchain has already done this check (except for the 1) |
| // but doing it again makes sure we don't miss it on unexpected code paths. |
| if !strings.HasPrefix(gotoolchain, "go1") && !strings.Contains(gotoolchain, "-go1") { |
| base.Fatalf("invalid GOTOOLCHAIN %q", gotoolchain) |
| } |
| |
| // 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.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 := &modcmd.ModuleJSON{ |
| Path: gotoolchainModule, |
| Version: gotoolchainVersion + "-" + gotoolchain + "." + runtime.GOOS + "-" + runtime.GOARCH, |
| } |
| modcmd.DownloadModule(context.Background(), m) |
| if m.Error != "" { |
| if strings.Contains(m.Error, ".info: 404") { |
| base.Fatalf("download %s for %s/%s: toolchain not available", gotoolchain, runtime.GOOS, runtime.GOARCH) |
| } |
| base.Fatalf("download %s: %v", gotoolchain, m.Error) |
| } |
| |
| // 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. |
| dir := m.Dir |
| 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")) |
| } |
| } |
| |
| // Reinvoke the go command. |
| execGoToolchain(gotoolchain, dir, filepath.Join(dir, "bin/go")) |
| } |
| |
| // execGoToolchain execs the Go toolchain with the given name (gotoolchain), |
| // GOROOT directory, and go command executable. |
| // The GOROOT directory is empty if we are invoking a command named |
| // gotoolchain found in $PATH. |
| func execGoToolchain(gotoolchain, dir, exe string) { |
| os.Setenv(gotoolchainSwitchEnv, "1") |
| if dir == "" { |
| os.Unsetenv("GOROOT") |
| } else { |
| os.Setenv("GOROOT", dir) |
| } |
| |
| // On Windows, there is no syscall.Exec, so the best we can do |
| // is run a subprocess and exit with the same status. |
| // Doing the same on Unix would be a problem because it wouldn't |
| // propagate signals and such, but there are no signals on Windows. |
| // We also use the exec case when GODEBUG=gotoolchainexec=0, |
| // to allow testing this code even when not on Windows. |
| if godebug.New("#gotoolchainexec").Value() == "0" || runtime.GOOS == "windows" { |
| cmd := exec.Command(exe, os.Args[1:]...) |
| cmd.Stdin = os.Stdin |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| fmt.Fprintln(os.Stderr, cmd.Args) |
| err := cmd.Run() |
| if err != nil { |
| if e, ok := err.(*exec.ExitError); ok && e.ProcessState != nil { |
| if e.ProcessState.Exited() { |
| os.Exit(e.ProcessState.ExitCode()) |
| } |
| base.Fatalf("exec %s: %s", gotoolchain, e.ProcessState) |
| } |
| base.Fatalf("exec %s: %s", exe, err) |
| } |
| os.Exit(0) |
| } |
| err := syscall.Exec(exe, os.Args, os.Environ()) |
| base.Fatalf("exec %s: %v", gotoolchain, err) |
| } |
| |
| // 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 looks at the command line to see if it is go install m@v or go run m@v. |
| // If so, it returns the m@v and the go version from that module's go.mod. |
| func goInstallVersion() (m module.Version, goVers string, ok 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 module.Version{}, "", false |
| } |
| |
| var arg string |
| switch os.Args[1] { |
| case "install": |
| // Cannot parse 'go install' command line precisely, because there |
| // may be new flags we don't know about. Instead, assume the final |
| // argument is a pkg@version we can use. |
| arg = os.Args[len(os.Args)-1] |
| case "run": |
| // For run, the pkg@version can be anywhere on the command line. |
| // We don't know the flags, so we can't strictly speaking do this correctly. |
| // We do the best we can by interrogating the CmdRun flags and assume |
| // that any unknown flag does not take an argument. |
| args := os.Args[2:] |
| for i := 0; i < len(args); i++ { |
| a := args[i] |
| if !strings.HasPrefix(a, "-") { |
| arg = a |
| break |
| } |
| if a == "-" { |
| break |
| } |
| if a == "--" { |
| if i+1 < len(args) { |
| arg = args[i+1] |
| } |
| break |
| } |
| a = strings.TrimPrefix(a, "-") |
| a = strings.TrimPrefix(a, "-") |
| if strings.HasPrefix(a, "-") { |
| // non-flag but also non-m@v |
| break |
| } |
| if strings.Contains(a, "=") { |
| // already has value |
| continue |
| } |
| f := run.CmdRun.Flag.Lookup(a) |
| if f == nil { |
| // Unknown flag. Assume it doesn't take a value: best we can do. |
| continue |
| } |
| 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 module.Version{}, "", false |
| } |
| m.Path, m.Version, _ = strings.Cut(arg, "@") |
| if m.Path == "" || m.Version == "" || gover.IsToolchain(m.Path) { |
| return module.Version{}, "", false |
| } |
| |
| // 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(m.Path, m.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, m.Path, m.Version, noneSelected, allowed) |
| tooNew, ok := err.(*gover.TooNewError) |
| if !ok { |
| return module.Version{}, "", false |
| } |
| m.Path, m.Version, _ = strings.Cut(tooNew.What, "@") |
| return m, tooNew.GoVersion, true |
| } |