gotip: explicitly set GOTOOLCHAIN=auto

Address issue 64665 by setting GOTOOLCHAIN=auto
if GOTOOLCHAIN env var is not explicitly configured
when invoking the real go binary from the gotip wrapper.
This ensures that user's global GOTOOLCHAIN setting
(set by `go env -w`) doesn't override gotip behavior.
Other version wrappers' behavior is left unchanged.

Fixes golang/go#64665

Change-Id: I076d34e88de8d149542085c494d4c1c31b7f379e
Reviewed-on: https://go-review.googlesource.com/c/dl/+/763260
Reviewed-by: Michael Matloob <matloob@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Michael Matloob <matloob@google.com>
diff --git a/gotip/main.go b/gotip/main.go
index 2b98428..f7e593e 100644
--- a/gotip/main.go
+++ b/gotip/main.go
@@ -14,6 +14,10 @@
 // To update, run "gotip download" again. This will always download the main branch.
 // To download an alternative branch, run "gotip download BRANCH".
 // To download a specific CL, run "gotip download NUMBER".
+//
+// By default, gotip force sets GOTOOLCHAIN=auto to avoid the GOTOOLCHAIN value
+// from go env -w interfere. Users can override this behavior by setting
+// GOTOOLCHAIN in their environment var setting.
 package main
 
 import (
diff --git a/internal/version/gotip.go b/internal/version/gotip.go
index 76ce7bb..764ddbe 100644
--- a/internal/version/gotip.go
+++ b/internal/version/gotip.go
@@ -16,6 +16,8 @@
 )
 
 // RunTip runs the "go" tool from the development tree.
+// It force sets GOTOOLCHAIN=auto to avoid the GOTOOLCHAIN value from go env -w interfere.
+// Users can override this behavior by setting GOTOOLCHAIN in their environment var setting.
 func RunTip() {
 	log.SetFlags(0)
 
@@ -46,7 +48,11 @@
 		log.Fatalf("gotip: not downloaded. Run 'gotip download' to install to %v", root)
 	}
 
-	runGo(root)
+	gotoolchain := ""
+	if _, ok := os.LookupEnv("GOTOOLCHAIN"); !ok {
+		gotoolchain = "auto"
+	}
+	runGo(root, gotoolchain)
 }
 
 func installTip(root, target string) error {
diff --git a/internal/version/version.go b/internal/version/version.go
index 783a2c2..1876bfe 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -51,20 +51,16 @@
 		log.Fatalf("%s: not downloaded. Run '%s download' to install to %v", version, version, root)
 	}
 
-	runGo(root)
+	runGo(root, "")
 }
 
-func runGo(root string) {
+func runGo(root, gotoolchain string) {
 	gobin := filepath.Join(root, "bin", "go"+exe())
 	cmd := exec.Command(gobin, os.Args[1:]...)
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
-	newPath := filepath.Join(root, "bin")
-	if p := os.Getenv("PATH"); p != "" {
-		newPath += string(filepath.ListSeparator) + p
-	}
-	cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))
+	cmd.Env = computeEnv(root, gotoolchain, os.Environ())
 
 	handleSignals()
 
@@ -77,6 +73,18 @@
 	os.Exit(0)
 }
 
+func computeEnv(root, gotoolchain string, baseEnv []string) []string {
+	newPath := filepath.Join(root, "bin")
+	if p := os.Getenv("PATH"); p != "" {
+		newPath += string(filepath.ListSeparator) + p
+	}
+	env := append(baseEnv, "GOROOT="+root, "PATH="+newPath)
+	if gotoolchain != "" {
+		env = append(env, "GOTOOLCHAIN="+gotoolchain)
+	}
+	return dedupEnv(caseInsensitiveEnv, env)
+}
+
 func fmtSize(size int64) string {
 	const (
 		byte_unit = 1 << (10 * iota)
diff --git a/internal/version/version_test.go b/internal/version/version_test.go
index 52545ca..e8f3b86 100644
--- a/internal/version/version_test.go
+++ b/internal/version/version_test.go
@@ -65,3 +65,57 @@
 		total *= 1024
 	}
 }
+
+func TestComputeEnv(t *testing.T) {
+	tests := []struct {
+		name        string
+		baseEnv     []string
+		gotoolchain string
+		wantContain string
+		wantAbsent  string
+	}{
+		{
+			name:        "not set, should use default",
+			baseEnv:     []string{},
+			gotoolchain: "auto",
+			wantContain: "GOTOOLCHAIN=auto",
+		},
+		{
+			name:        "already set, should override",
+			baseEnv:     []string{"GOTOOLCHAIN=user-value"},
+			gotoolchain: "auto",
+			wantContain: "GOTOOLCHAIN=auto",
+			wantAbsent:  "GOTOOLCHAIN=user-value",
+		},
+		{
+			name:        "gotoolchain empty, should not add",
+			baseEnv:     []string{},
+			gotoolchain: "",
+			wantAbsent:  "GOTOOLCHAIN=",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			env := computeEnv("/tmp/goroot", tt.gotoolchain, tt.baseEnv)
+
+			foundContain := false
+			foundAbsent := false
+			for _, e := range env {
+				if tt.wantContain != "" && e == tt.wantContain {
+					foundContain = true
+				}
+				if tt.wantAbsent != "" && e == tt.wantAbsent {
+					foundAbsent = true
+				}
+			}
+
+			if tt.wantContain != "" && !foundContain {
+				t.Errorf("expected env to contain %q, but it didn't. Env: %v", tt.wantContain, env)
+			}
+			if tt.wantAbsent != "" && foundAbsent {
+				t.Errorf("expected env to NOT contain %q, but it did. Env: %v", tt.wantAbsent, env)
+			}
+		})
+	}
+}