// 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
}
