all: consistently set PWD to match Dir for subprocesses

Also consistently deduplicate Env entries, mainly to reduce confusion
when reading logs.

This change is probably larger than what is strictly needed to fix the
issue, but it seemed simpler than trying to figure out which of the
many calls to exec.Command in the module are actually relevant.
(It also provides some useful case studies for possible additions to
the Go standard library.)

For golang/go#33598

Change-Id: Ia2bce4549e4a71b56fb497e3df093f79fbcf7f29
Reviewed-on: https://go-review.googlesource.com/c/build/+/353549
Trust: Bryan C. Mills <bcmills@google.com>
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/cloudfns/sendwikidiff/sendwikidiff.go b/cloudfns/sendwikidiff/sendwikidiff.go
index 694a4df..65e6923 100644
--- a/cloudfns/sendwikidiff/sendwikidiff.go
+++ b/cloudfns/sendwikidiff/sendwikidiff.go
@@ -139,6 +139,7 @@
 	defer r.Unlock()
 	cmd := exec.Command("git", "pull")
 	cmd.Dir = r.dir
+	cmd.Env = append(os.Environ(), "PWD="+r.dir)
 	cmd.Stderr = os.Stderr
 	cmd.Stdout = os.Stdout
 	if err := cmd.Run(); err != nil {
@@ -169,5 +170,6 @@
 	defer r.RUnlock()
 	cmd := exec.Command("git", "show", ref)
 	cmd.Dir = r.dir
+	cmd.Env = append(os.Environ(), "PWD="+r.dir)
 	return cmd
 }
diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go
index 4985c6e..0215691 100644
--- a/cmd/buildlet/buildlet.go
+++ b/cmd/buildlet/buildlet.go
@@ -46,6 +46,7 @@
 	"github.com/aws/aws-sdk-go/aws/session"
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/internal/cloud"
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/pargzip"
 )
 
@@ -215,8 +216,6 @@
 		removeAllAndMkdir(processGoCacheEnv)
 	}
 
-	initGorootBootstrap()
-
 	http.HandleFunc("/", handleRoot)
 	http.HandleFunc("/debug/x", handleX)
 
@@ -261,17 +260,6 @@
 	}
 }
 
-var inheritedGorootBootstrap string
-
-func initGorootBootstrap() {
-	// Remember any GOROOT_BOOTSTRAP to use as a backup in handleExec
-	// if $WORKDIR/go1.4 ends up not existing.
-	inheritedGorootBootstrap = os.Getenv("GOROOT_BOOTSTRAP")
-
-	// Default if not otherwise configured in dashboard/builders.go:
-	os.Setenv("GOROOT_BOOTSTRAP", filepath.Join(*workDir, "go1.4"))
-}
-
 func listenForCoordinator() {
 	tlsCert, tlsKey := metadataValue(metaKeyTLSCert), metadataValue(metaKeyTLSkey)
 	if (tlsCert == "") != (tlsKey == "") {
@@ -930,28 +918,22 @@
 	postEnv := r.PostForm["env"]
 
 	goarch := "amd64" // unless we find otherwise
-	if v := getEnv(postEnv, "GOARCH"); v != "" {
+	if v := envutil.Get(runtime.GOOS, postEnv, "GOARCH"); v != "" {
 		goarch = v
 	}
-	if v, _ := strconv.ParseBool(getEnv(postEnv, "GO_DISABLE_OUTBOUND_NETWORK")); v {
+	if v, _ := strconv.ParseBool(envutil.Get(runtime.GOOS, postEnv, "GO_DISABLE_OUTBOUND_NETWORK")); v {
 		disableOutboundNetwork()
 	}
 
 	env := append(baseEnv(goarch), postEnv...)
-
 	if v := processTmpDirEnv; v != "" {
 		env = append(env, "TMPDIR="+v)
 	}
 	if v := processGoCacheEnv; v != "" {
 		env = append(env, "GOCACHE="+v)
 	}
-
-	// Prefer buildlet process's inherited GOROOT_BOOTSTRAP if
-	// there was one and the one we're about to use doesn't exist.
-	if v := getEnv(env, "GOROOT_BOOTSTRAP"); v != "" && inheritedGorootBootstrap != "" && pathNotExist(v) {
-		env = append(env, "GOROOT_BOOTSTRAP="+inheritedGorootBootstrap)
-	}
 	env = setPathEnv(env, r.PostForm["path"], *workDir)
+	env = envutil.Dedup(runtime.GOOS, env)
 
 	var cmd *exec.Cmd
 	if needsBashWrapper(absCmd) {
@@ -960,11 +942,11 @@
 		cmd = exec.Command(absCmd)
 	}
 	cmd.Args = append(cmd.Args, r.PostForm["cmdArg"]...)
-	cmd.Dir = dir
+	cmd.Env = env
+	envutil.SetDir(cmd, dir)
 	cmdOutput := flushWriter{w}
 	cmd.Stdout = cmdOutput
 	cmd.Stderr = cmdOutput
-	cmd.Env = env
 
 	log.Printf("[%p] Running %s with args %q and env %q in dir %s",
 		cmd, cmd.Path, cmd.Args, cmd.Env, cmd.Dir)
@@ -1019,26 +1001,6 @@
 	return os.IsNotExist(err)
 }
 
-func getEnv(env []string, key string) string {
-	for _, kv := range env {
-		if len(kv) <= len(key) || kv[len(key)] != '=' {
-			continue
-		}
-		if runtime.GOOS == "windows" {
-			// Case insensitive.
-			if strings.EqualFold(kv[:len(key)], key) {
-				return kv[len(key)+1:]
-			}
-		} else {
-			// Case sensitive.
-			if kv[:len(key)] == key {
-				return kv[len(key)+1:]
-			}
-		}
-	}
-	return ""
-}
-
 // setPathEnv returns a copy of the provided environment with any existing
 // PATH variables replaced by the user-provided path.
 // These substitutions are applied to user-supplied path elements:
@@ -1125,11 +1087,33 @@
 	}
 }
 
+var (
+	defaultBootstrap     string
+	defaultBootstrapOnce sync.Once
+)
+
 func baseEnv(goarch string) []string {
+	var env []string
 	if runtime.GOOS == "windows" {
-		return windowsBaseEnv(goarch)
+		env = windowsBaseEnv(goarch)
+	} else {
+		env = os.Environ()
 	}
-	return os.Environ()
+
+	defaultBootstrapOnce.Do(func() {
+		defaultBootstrap = filepath.Join(*workDir, "go1.4")
+
+		// Prefer buildlet process's inherited GOROOT_BOOTSTRAP if
+		// there was one and our default doesn't exist.
+		if v := os.Getenv("GOROOT_BOOTSTRAP"); v != "" && v != defaultBootstrap {
+			if pathNotExist(defaultBootstrap) {
+				defaultBootstrap = v
+			}
+		}
+	})
+	env = append(env, "GOROOT_BOOTSTRAP="+defaultBootstrap)
+
+	return env
 }
 
 func windowsBaseEnv(goarch string) (e []string) {
diff --git a/cmd/coordinator/remote.go b/cmd/coordinator/remote.go
index c3e0e62..ca0d75e 100644
--- a/cmd/coordinator/remote.go
+++ b/cmd/coordinator/remote.go
@@ -39,6 +39,7 @@
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/dashboard"
 	"golang.org/x/build/internal/coordinator/pool"
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/internal/gophers"
 	"golang.org/x/build/internal/secret"
 	"golang.org/x/build/types"
@@ -742,7 +743,7 @@
 		cmd = exec.Command("/usr/local/bin/drawterm",
 			"-a", ip, "-c", ip, "-u", "glenda", "-k", "user=glenda")
 	}
-	cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
+	envutil.SetEnv(cmd, "TERM="+ptyReq.Term)
 	f, err := pty.Start(cmd)
 	if err != nil {
 		log.Printf("running ssh client to %s: %v", inst, err)
diff --git a/cmd/genbootstrap/genbootstrap.go b/cmd/genbootstrap/genbootstrap.go
index 7dc94b4..2dcb93a 100644
--- a/cmd/genbootstrap/genbootstrap.go
+++ b/cmd/genbootstrap/genbootstrap.go
@@ -20,6 +20,8 @@
 	"os/exec"
 	"path/filepath"
 	"strings"
+
+	"golang.org/x/build/internal/envutil"
 )
 
 var skipBuild = flag.Bool("skip_build", false, "skip bootstrap.bash step; useful during development of cleaning code")
@@ -52,8 +54,8 @@
 	if !*skipBuild {
 		os.RemoveAll(outDir)
 		cmd := exec.Command(filepath.Join(os.Getenv("GOROOT"), "src", "bootstrap.bash"))
-		cmd.Dir = filepath.Join(os.Getenv("GOROOT"), "src")
-		cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
+		envutil.SetDir(cmd, filepath.Join(os.Getenv("GOROOT"), "src"))
+		envutil.SetEnv(cmd, "GOOS="+goos, "GOARCH="+goarch)
 		cmd.Stdout = os.Stdout
 		cmd.Stderr = os.Stderr
 		if err := cmd.Run(); err != nil {
@@ -133,7 +135,7 @@
 
 	log.Printf("Running: tar zcf %s .", tgz)
 	cmd := exec.Command("tar", "zcf", tgz, ".")
-	cmd.Dir = outDir
+	envutil.SetDir(cmd, outDir)
 	if err := cmd.Run(); err != nil {
 		log.Fatalf("tar zf failed: %v", err)
 	}
diff --git a/cmd/gitmirror/gitmirror.go b/cmd/gitmirror/gitmirror.go
index 84ea914..8000c11 100644
--- a/cmd/gitmirror/gitmirror.go
+++ b/cmd/gitmirror/gitmirror.go
@@ -28,6 +28,7 @@
 	"time"
 
 	"golang.org/x/build/gerrit"
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/internal/gitauth"
 	"golang.org/x/build/internal/secret"
 	"golang.org/x/build/maintner"
@@ -385,11 +386,11 @@
 	cmd := exec.Command("git", args...)
 	if args[0] == "clone" {
 		// Small hack: if we're cloning, the root doesn't exist yet.
-		cmd.Dir = "/"
+		envutil.SetDir(cmd, "/")
 	} else {
-		cmd.Dir = r.root
+		envutil.SetDir(cmd, r.root)
 	}
-	cmd.Env = append(os.Environ(), "HOME="+r.mirror.homeDir)
+	envutil.SetEnv(cmd, "HOME="+r.mirror.homeDir)
 	cmd.Stdout, cmd.Stderr = stdout, stderr
 	err := runCmdContext(ctx, cmd)
 	return stdout.Bytes(), stderr.Bytes(), err
diff --git a/cmd/gitmirror/gitmirror_test.go b/cmd/gitmirror/gitmirror_test.go
index 344b5f0..cb121e9 100644
--- a/cmd/gitmirror/gitmirror_test.go
+++ b/cmd/gitmirror/gitmirror_test.go
@@ -16,6 +16,7 @@
 	"strings"
 	"testing"
 
+	"golang.org/x/build/internal/envutil"
 	repospkg "golang.org/x/build/repos"
 )
 
@@ -190,7 +191,7 @@
 func (tm *testMirror) git(dir string, args ...string) string {
 	tm.t.Helper()
 	cmd := exec.Command("git", args...)
-	cmd.Dir = dir
+	envutil.SetDir(cmd, dir)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		tm.t.Fatalf("git: %v, %s", err, out)
diff --git a/cmd/gomote/run.go b/cmd/gomote/run.go
index d05409b..be22312 100644
--- a/cmd/gomote/run.go
+++ b/cmd/gomote/run.go
@@ -13,7 +13,7 @@
 
 	"golang.org/x/build/buildlet"
 	"golang.org/x/build/dashboard"
-	"golang.org/x/build/envutil"
+	"golang.org/x/build/internal/envutil"
 )
 
 func run(args []string) error {
@@ -77,7 +77,7 @@
 		SystemLevel: sys || strings.HasPrefix(cmd, "/"),
 		Output:      os.Stdout,
 		Args:        fs.Args()[2:],
-		ExtraEnv:    envutil.Dedup(conf.GOOS() == "windows", append(conf.Env(), []string(env)...)),
+		ExtraEnv:    envutil.Dedup(conf.GOOS(), append(conf.Env(), []string(env)...)),
 		Debug:       debug,
 		Path:        pathOpt,
 	})
diff --git a/cmd/racebuild/racebuild.go b/cmd/racebuild/racebuild.go
index 4e439ff..8059525 100644
--- a/cmd/racebuild/racebuild.go
+++ b/cmd/racebuild/racebuild.go
@@ -25,6 +25,7 @@
 	"strings"
 	"sync"
 
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/sync/errgroup"
 )
 
@@ -273,7 +274,7 @@
 	parsePlatformsFlag()
 
 	cmd := exec.Command("git", "rev-parse", *flagGoRev)
-	cmd.Dir = *flagGoroot
+	envutil.SetDir(cmd, *flagGoroot)
 	cmd.Stderr = os.Stderr
 	out, err := cmd.Output()
 	if err != nil {
diff --git a/cmd/release/releaselet.go b/cmd/release/releaselet.go
index 4a3fde0..7866162 100644
--- a/cmd/release/releaselet.go
+++ b/cmd/release/releaselet.go
@@ -252,6 +252,9 @@
 func runDir(dir, name string, arg ...string) error {
 	cmd := exec.Command(name, arg...)
 	cmd.Dir = dir
+	if dir != "" {
+		cmd.Env = append(os.Environ(), "PWD="+dir)
+	}
 	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
 	return cmd.Run()
 }
diff --git a/cmd/release/releaselet_test.go b/cmd/release/releaselet_test.go
index a9ed794..376814b 100644
--- a/cmd/release/releaselet_test.go
+++ b/cmd/release/releaselet_test.go
@@ -7,15 +7,16 @@
 import (
 	"bytes"
 	"io/ioutil"
-	"os"
 	"os/exec"
 	"strings"
 	"testing"
+
+	"golang.org/x/build/internal/envutil"
 )
 
 func TestReleaselet(t *testing.T) {
 	cmd := exec.Command("go", "run", "releaselet.go")
-	cmd.Env = append(os.Environ(), "RUN_RELEASELET_TESTS=true")
+	envutil.SetEnv(cmd, "RUN_RELEASELET_TESTS=true")
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		t.Fatalf("error running releaselet.go tests: %v, %s", err, out)
diff --git a/cmd/release/static.go b/cmd/release/static.go
index 9a804ed..906adf2 100644
--- a/cmd/release/static.go
+++ b/cmd/release/static.go
@@ -6,4 +6,4 @@
 
 package main
 
-const releaselet = "// Copyright 2015 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build ignore\n// +build ignore\n\n// Command releaselet does buildlet-side release construction tasks.\n// It is intended to be executed on the buildlet preparing a release.\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc main() {\n\tif v, _ := strconv.ParseBool(os.Getenv(\"RUN_RELEASELET_TESTS\")); v {\n\t\trunSelfTests()\n\t\treturn\n\t}\n\n\tif dir := archDir(); dir != \"\" {\n\t\tif err := cp(\"go/bin/go\", \"go/bin/\"+dir+\"/go\"); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tif err := cp(\"go/bin/gofmt\", \"go/bin/\"+dir+\"/gofmt\"); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tos.RemoveAll(\"go/bin/\" + dir)\n\t\tos.RemoveAll(\"go/pkg/linux_amd64\")\n\t\tos.RemoveAll(\"go/pkg/tool/linux_amd64\")\n\t}\n\tos.RemoveAll(\"go/pkg/obj\")\n\tif runtime.GOOS == \"windows\" {\n\t\t// Clean up .exe~ files; golang.org/issue/23894\n\t\tfilepath.Walk(\"go\", func(path string, fi os.FileInfo, err error) error {\n\t\t\tif strings.HasSuffix(path, \".exe~\") {\n\t\t\t\tos.Remove(path)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err := windowsMSI(); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc archDir() string {\n\tif os.Getenv(\"GO_BUILDER_NAME\") == \"linux-s390x-crosscompile\" {\n\t\treturn \"linux_s390x\"\n\t}\n\treturn \"\"\n}\n\nfunc environ() (cwd, version string, err error) {\n\tcwd, err = os.Getwd()\n\tif err != nil {\n\t\treturn\n\t}\n\tvar versionBytes []byte\n\tversionBytes, err = ioutil.ReadFile(\"go/VERSION\")\n\tif err != nil {\n\t\treturn\n\t}\n\tversion = string(bytes.TrimSpace(versionBytes))\n\treturn\n}\n\nfunc windowsMSI() error {\n\tcwd, version, err := environ()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Install Wix tools.\n\twix := filepath.Join(cwd, \"wix\")\n\tdefer os.RemoveAll(wix)\n\tswitch runtime.GOARCH {\n\tdefault:\n\t\tif err := installWix(wixRelease311, wix); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"arm64\":\n\t\tif err := installWix(wixRelease314, wix); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Write out windows data that is used by the packaging process.\n\twin := filepath.Join(cwd, \"windows\")\n\tdefer os.RemoveAll(win)\n\tif err := writeDataFiles(windowsData, win); err != nil {\n\t\treturn err\n\t}\n\n\t// Gather files.\n\tgoDir := filepath.Join(cwd, \"go\")\n\tappfiles := filepath.Join(win, \"AppFiles.wxs\")\n\tif err := runDir(win, filepath.Join(wix, \"heat\"),\n\t\t\"dir\", goDir,\n\t\t\"-nologo\",\n\t\t\"-gg\", \"-g1\", \"-srd\", \"-sfrag\",\n\t\t\"-cg\", \"AppFiles\",\n\t\t\"-template\", \"fragment\",\n\t\t\"-dr\", \"INSTALLDIR\",\n\t\t\"-var\", \"var.SourceDir\",\n\t\t\"-out\", appfiles,\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tmsArch := func() string {\n\t\tswitch runtime.GOARCH {\n\t\tdefault:\n\t\t\tpanic(\"unknown arch for windows \" + runtime.GOARCH)\n\t\tcase \"386\":\n\t\t\treturn \"x86\"\n\t\tcase \"amd64\":\n\t\t\treturn \"x64\"\n\t\tcase \"arm64\":\n\t\t\treturn \"arm64\"\n\t\t}\n\t}\n\n\t// Build package.\n\tverMajor, verMinor, verPatch := splitVersion(version)\n\n\tif err := runDir(win, filepath.Join(wix, \"candle\"),\n\t\t\"-nologo\",\n\t\t\"-arch\", msArch(),\n\t\t\"-dGoVersion=\"+version,\n\t\tfmt.Sprintf(\"-dWixGoVersion=%v.%v.%v\", verMajor, verMinor, verPatch),\n\t\t\"-dArch=\"+runtime.GOARCH,\n\t\t\"-dSourceDir=\"+goDir,\n\t\tfilepath.Join(win, \"installer.wxs\"),\n\t\tappfiles,\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tmsi := filepath.Join(cwd, \"msi\") // known to cmd/release\n\tif err := os.Mkdir(msi, 0755); err != nil {\n\t\treturn err\n\t}\n\treturn runDir(win, filepath.Join(wix, \"light\"),\n\t\t\"-nologo\",\n\t\t\"-dcl:high\",\n\t\t\"-ext\", \"WixUIExtension\",\n\t\t\"-ext\", \"WixUtilExtension\",\n\t\t\"AppFiles.wixobj\",\n\t\t\"installer.wixobj\",\n\t\t\"-o\", filepath.Join(msi, \"go.msi\"), // file name irrelevant\n\t)\n}\n\ntype wixRelease struct {\n\tBinaryURL string\n\tSHA256    string\n}\n\nvar (\n\twixRelease311 = wixRelease{\n\t\tBinaryURL: \"https://storage.googleapis.com/go-builder-data/wix311-binaries.zip\",\n\t\tSHA256:    \"da034c489bd1dd6d8e1623675bf5e899f32d74d6d8312f8dd125a084543193de\",\n\t}\n\twixRelease314 = wixRelease{\n\t\tBinaryURL: \"https://storage.googleapis.com/go-builder-data/wix314-binaries.zip\",\n\t\tSHA256:    \"34dcbba9952902bfb710161bd45ee2e721ffa878db99f738285a21c9b09c6edb\", // WiX v3.14.0.4118 release, SHA 256 of wix314-binaries.zip from https://wixtoolset.org/releases/v3-14-0-4118/.\n\t}\n)\n\n// installWix fetches and installs the wix toolkit to the specified path.\nfunc installWix(wix wixRelease, path string) error {\n\t// Fetch wix binary zip file.\n\tbody, err := httpGet(wix.BinaryURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Verify sha256.\n\tsum := sha256.Sum256(body)\n\tif fmt.Sprintf(\"%x\", sum) != wix.SHA256 {\n\t\treturn errors.New(\"sha256 mismatch for wix toolkit\")\n\t}\n\n\t// Unzip to path.\n\tzr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range zr.File {\n\t\tname := filepath.FromSlash(f.Name)\n\t\terr := os.MkdirAll(filepath.Join(path, filepath.Dir(name)), 0755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb, err := ioutil.ReadAll(rc)\n\t\trc.Close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = ioutil.WriteFile(filepath.Join(path, name), b, 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc httpGet(url string) ([]byte, error) {\n\tr, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbody, err := ioutil.ReadAll(r.Body)\n\tr.Body.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r.StatusCode != 200 {\n\t\treturn nil, errors.New(r.Status)\n\t}\n\treturn body, nil\n}\n\nfunc run(name string, arg ...string) error {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr\n\treturn cmd.Run()\n}\n\nfunc runDir(dir, name string, arg ...string) error {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Dir = dir\n\tcmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr\n\treturn cmd.Run()\n}\n\nfunc cp(dst, src string) error {\n\tsf, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sf.Close()\n\tfi, err := sf.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\ttmpDst := dst + \".tmp\"\n\tdf, err := os.Create(tmpDst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer df.Close()\n\t// Windows doesn't implement Fchmod.\n\tif runtime.GOOS != \"windows\" {\n\t\tif err := df.Chmod(fi.Mode()); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err = io.Copy(df, sf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := df.Close(); err != nil {\n\t\treturn err\n\t}\n\tif err := os.Rename(tmpDst, dst); err != nil {\n\t\treturn err\n\t}\n\t// Ensure the destination has the same mtime as the source.\n\treturn os.Chtimes(dst, fi.ModTime(), fi.ModTime())\n}\n\nfunc cpDir(dst, src string) error {\n\twalk := func(srcPath string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdstPath := filepath.Join(dst, srcPath[len(src):])\n\t\tif info.IsDir() {\n\t\t\treturn os.MkdirAll(dstPath, 0755)\n\t\t}\n\t\treturn cp(dstPath, srcPath)\n\t}\n\treturn filepath.Walk(src, walk)\n}\n\nfunc cpAllDir(dst, basePath string, dirs ...string) error {\n\tfor _, dir := range dirs {\n\t\tif err := cpDir(filepath.Join(dst, dir), filepath.Join(basePath, dir)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ext() string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \".exe\"\n\t}\n\treturn \"\"\n}\n\nvar versionRe = regexp.MustCompile(`^go(\\d+(\\.\\d+)*)`)\n\n// splitVersion splits a Go version string such as \"go1.9\" or \"go1.10.2\" (as matched by versionRe)\n// into its three parts: major, minor, and patch\n// It's based on the Git tag.\nfunc splitVersion(v string) (major, minor, patch int) {\n\tm := versionRe.FindStringSubmatch(v)\n\tif m == nil {\n\t\treturn\n\t}\n\tparts := strings.Split(m[1], \".\")\n\tif len(parts) >= 1 {\n\t\tmajor, _ = strconv.Atoi(parts[0])\n\n\t\tif len(parts) >= 2 {\n\t\t\tminor, _ = strconv.Atoi(parts[1])\n\n\t\t\tif len(parts) >= 3 {\n\t\t\t\tpatch, _ = strconv.Atoi(parts[2])\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nconst storageBase = \"https://storage.googleapis.com/go-builder-data/release/\"\n\n// writeDataFiles writes the files in the provided map to the provided base\n// directory. If the map value is a URL it fetches the data at that URL and\n// uses it as the file contents.\nfunc writeDataFiles(data map[string]string, base string) error {\n\tfor name, body := range data {\n\t\tdst := filepath.Join(base, name)\n\t\terr := os.MkdirAll(filepath.Dir(dst), 0755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb := []byte(body)\n\t\tif strings.HasPrefix(body, storageBase) {\n\t\t\tb, err = httpGet(body)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t// (We really mean 0755 on the next line; some of these files\n\t\t// are executable, and there's no harm in making them all so.)\n\t\tif err := ioutil.WriteFile(dst, b, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nvar windowsData = map[string]string{\n\n\t\"installer.wxs\": `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n<!--\n# Copyright 2010 The Go Authors. All rights reserved.\n# Use of this source code is governed by a BSD-style\n# license that can be found in the LICENSE file.\n-->\n\n<?if $(var.Arch) = 386 ?>\n  <?define UpgradeCode = {1C3114EA-08C3-11E1-9095-7FCA4824019B} ?>\n  <?define InstallerVersion=\"300\" ?>\n  <?define SysFolder=SystemFolder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFilesFolder\" ?>\n<?elseif $(var.Arch) = arm64 ?>\n  <?define UpgradeCode = {21ade9a3-3fdd-4ba6-bea6-c85abadc9488} ?>\n  <?define InstallerVersion=\"500\" ?>\n  <?define SysFolder=System64Folder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFiles64Folder\" ?>\n<?else?>\n  <?define UpgradeCode = {22ea7650-4ac6-4001-bf29-f4b8775db1c0} ?>\n  <?define InstallerVersion=\"300\" ?>\n  <?define SysFolder=System64Folder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFiles64Folder\" ?>\n<?endif?>\n\n<Product\n    Id=\"*\"\n    Name=\"Go Programming Language $(var.Arch) $(var.GoVersion)\"\n    Language=\"1033\"\n    Version=\"$(var.WixGoVersion)\"\n    Manufacturer=\"https://golang.org\"\n    UpgradeCode=\"$(var.UpgradeCode)\" >\n\n<Package\n    Id='*'\n    Keywords='Installer'\n    Description=\"The Go Programming Language Installer\"\n    Comments=\"The Go programming language is an open source project to make programmers more productive.\"\n    InstallerVersion=\"$(var.InstallerVersion)\"\n    Compressed=\"yes\"\n    InstallScope=\"perMachine\"\n    Languages=\"1033\" />\n\n<Property Id=\"ARPCOMMENTS\" Value=\"The Go programming language is a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.\" />\n<Property Id=\"ARPCONTACT\" Value=\"golang-nuts@googlegroups.com\" />\n<Property Id=\"ARPHELPLINK\" Value=\"https://golang.org/help/\" />\n<Property Id=\"ARPREADME\" Value=\"https://golang.org\" />\n<Property Id=\"ARPURLINFOABOUT\" Value=\"https://golang.org\" />\n<Property Id=\"LicenseAccepted\">1</Property>\n<Icon Id=\"gopher.ico\" SourceFile=\"images\\gopher.ico\"/>\n<Property Id=\"ARPPRODUCTICON\" Value=\"gopher.ico\" />\n<Property Id=\"EXISTING_GOLANG_INSTALLED\">\n  <RegistrySearch Id=\"installed\" Type=\"raw\" Root=\"HKCU\" Key=\"Software\\GoProgrammingLanguage\" Name=\"installed\" />\n</Property>\n<Media Id='1' Cabinet=\"go.cab\" EmbedCab=\"yes\" CompressionLevel=\"high\" />\n<Condition Message=\"Windows 7 (with Service Pack 1) or greater required.\">\n    ((VersionNT > 601) OR (VersionNT = 601 AND ServicePackLevel >= 1))\n</Condition>\n<MajorUpgrade AllowDowngrades=\"yes\" />\n\n<CustomAction\n    Id=\"SetApplicationRootDirectory\"\n    Property=\"ARPINSTALLLOCATION\"\n    Value=\"[INSTALLDIR]\" />\n\n<!-- Define the directory structure and environment variables -->\n<Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n  <Directory Id=\"$(var.ArchProgramFilesFolder)\">\n    <Directory Id=\"INSTALLDIR\" Name=\"Go\"/>\n  </Directory>\n  <Directory Id=\"ProgramMenuFolder\">\n    <Directory Id=\"GoProgramShortcutsDir\" Name=\"Go Programming Language\"/>\n  </Directory>\n  <Directory Id=\"EnvironmentEntries\">\n    <Directory Id=\"GoEnvironmentEntries\" Name=\"Go Programming Language\"/>\n  </Directory>\n</Directory>\n\n<!-- Programs Menu Shortcuts -->\n<DirectoryRef Id=\"GoProgramShortcutsDir\">\n  <Component Id=\"Component_GoProgramShortCuts\" Guid=\"{f5fbfb5e-6c5c-423b-9298-21b0e3c98f4b}\">\n    <Shortcut\n        Id=\"UninstallShortcut\"\n        Name=\"Uninstall Go\"\n        Description=\"Uninstalls Go and all of its components\"\n        Target=\"[$(var.SysFolder)]msiexec.exe\"\n        Arguments=\"/x [ProductCode]\" />\n    <RemoveFolder\n        Id=\"GoProgramShortcutsDir\"\n        On=\"uninstall\" />\n    <RegistryValue\n        Root=\"HKCU\"\n        Key=\"Software\\GoProgrammingLanguage\"\n        Name=\"ShortCuts\"\n        Type=\"integer\"\n        Value=\"1\"\n        KeyPath=\"yes\" />\n  </Component>\n</DirectoryRef>\n\n<!-- Registry & Environment Settings -->\n<DirectoryRef Id=\"GoEnvironmentEntries\">\n  <Component Id=\"Component_GoEnvironment\" Guid=\"{3ec7a4d5-eb08-4de7-9312-2df392c45993}\">\n    <RegistryKey\n        Root=\"HKCU\"\n        Key=\"Software\\GoProgrammingLanguage\">\n            <RegistryValue\n                Name=\"installed\"\n                Type=\"integer\"\n                Value=\"1\"\n                KeyPath=\"yes\" />\n            <RegistryValue\n                Name=\"installLocation\"\n                Type=\"string\"\n                Value=\"[INSTALLDIR]\" />\n    </RegistryKey>\n    <Environment\n        Id=\"GoPathEntry\"\n        Action=\"set\"\n        Part=\"last\"\n        Name=\"PATH\"\n        Permanent=\"no\"\n        System=\"yes\"\n        Value=\"[INSTALLDIR]bin\" />\n    <Environment\n        Id=\"UserGoPath\"\n        Action=\"create\"\n        Name=\"GOPATH\"\n        Permanent=\"no\"\n        Value=\"%USERPROFILE%\\go\" />\n    <Environment\n        Id=\"UserGoPathEntry\"\n        Action=\"set\"\n        Part=\"last\"\n        Name=\"PATH\"\n        Permanent=\"no\"\n        Value=\"%USERPROFILE%\\go\\bin\" />\n    <RemoveFolder\n        Id=\"GoEnvironmentEntries\"\n        On=\"uninstall\" />\n  </Component>\n</DirectoryRef>\n\n<!-- Install the files -->\n<Feature\n    Id=\"GoTools\"\n    Title=\"Go\"\n    Level=\"1\">\n      <ComponentRef Id=\"Component_GoEnvironment\" />\n      <ComponentGroupRef Id=\"AppFiles\" />\n      <ComponentRef Id=\"Component_GoProgramShortCuts\" />\n</Feature>\n\n<!-- Update the environment -->\n<InstallExecuteSequence>\n    <Custom Action=\"SetApplicationRootDirectory\" Before=\"InstallFinalize\" />\n</InstallExecuteSequence>\n\n<!-- Notify top level applications of the new PATH variable (golang.org/issue/18680)  -->\n<CustomActionRef Id=\"WixBroadcastEnvironmentChange\" />\n\n<!-- Include the user interface -->\n<WixVariable Id=\"WixUILicenseRtf\" Value=\"LICENSE.rtf\" />\n<WixVariable Id=\"WixUIBannerBmp\" Value=\"images\\Banner.jpg\" />\n<WixVariable Id=\"WixUIDialogBmp\" Value=\"images\\Dialog.jpg\" />\n<Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALLDIR\" />\n<UIRef Id=\"Golang_InstallDir\" />\n<UIRef Id=\"WixUI_ErrorProgressText\" />\n\n</Product>\n<Fragment>\n  <!--\n    The installer steps are modified so we can get user confirmation to uninstall an existing golang installation.\n\n    WelcomeDlg  [not installed]  =>                  LicenseAgreementDlg => InstallDirDlg  ..\n                [installed]      => OldVersionDlg => LicenseAgreementDlg => InstallDirDlg  ..\n  -->\n  <UI Id=\"Golang_InstallDir\">\n    <!-- style -->\n    <TextStyle Id=\"WixUI_Font_Normal\" FaceName=\"Tahoma\" Size=\"8\" />\n    <TextStyle Id=\"WixUI_Font_Bigger\" FaceName=\"Tahoma\" Size=\"12\" />\n    <TextStyle Id=\"WixUI_Font_Title\" FaceName=\"Tahoma\" Size=\"9\" Bold=\"yes\" />\n\n    <Property Id=\"DefaultUIFont\" Value=\"WixUI_Font_Normal\" />\n    <Property Id=\"WixUI_Mode\" Value=\"InstallDir\" />\n\n    <!-- dialogs -->\n    <DialogRef Id=\"BrowseDlg\" />\n    <DialogRef Id=\"DiskCostDlg\" />\n    <DialogRef Id=\"ErrorDlg\" />\n    <DialogRef Id=\"FatalError\" />\n    <DialogRef Id=\"FilesInUse\" />\n    <DialogRef Id=\"MsiRMFilesInUse\" />\n    <DialogRef Id=\"PrepareDlg\" />\n    <DialogRef Id=\"ProgressDlg\" />\n    <DialogRef Id=\"ResumeDlg\" />\n    <DialogRef Id=\"UserExit\" />\n    <Dialog Id=\"OldVersionDlg\" Width=\"240\" Height=\"95\" Title=\"[ProductName] Setup\" NoMinimize=\"yes\">\n      <Control Id=\"Text\" Type=\"Text\" X=\"28\" Y=\"15\" Width=\"194\" Height=\"50\">\n        <Text>A previous version of Go Programming Language is currently installed. By continuing the installation this version will be uninstalled. Do you want to continue?</Text>\n      </Control>\n      <Control Id=\"Exit\" Type=\"PushButton\" X=\"123\" Y=\"67\" Width=\"62\" Height=\"17\"\n        Default=\"yes\" Cancel=\"yes\" Text=\"No, Exit\">\n        <Publish Event=\"EndDialog\" Value=\"Exit\">1</Publish>\n      </Control>\n      <Control Id=\"Next\" Type=\"PushButton\" X=\"55\" Y=\"67\" Width=\"62\" Height=\"17\" Text=\"Yes, Uninstall\">\n        <Publish Event=\"EndDialog\" Value=\"Return\">1</Publish>\n      </Control>\n    </Dialog>\n\n    <!-- wizard steps -->\n    <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"3\">1</Publish>\n    <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"4\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n\n    <Publish Dialog=\"ExitDialog\" Control=\"Finish\" Event=\"EndDialog\" Value=\"Return\" Order=\"999\">1</Publish>\n\n    <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"OldVersionDlg\"><![CDATA[EXISTING_GOLANG_INSTALLED << \"#1\"]]> </Publish>\n    <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\"><![CDATA[NOT (EXISTING_GOLANG_INSTALLED << \"#1\")]]></Publish>\n\n    <Publish Dialog=\"OldVersionDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">1</Publish>\n\n    <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\">1</Publish>\n    <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"InstallDirDlg\">LicenseAccepted = \"1\"</Publish>\n\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SetTargetPath\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"2\">NOT WIXUI_DONTVALIDATEPATH</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"3\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\" Order=\"4\">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=\"1\"</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Property=\"_BrowseProperty\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Event=\"SpawnDialog\" Value=\"BrowseDlg\" Order=\"2\">1</Publish>\n\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"InstallDirDlg\" Order=\"1\">NOT Installed</Publish>\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\" Order=\"2\">Installed AND NOT PATCH</Publish>\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\" Order=\"2\">Installed AND PATCH</Publish>\n\n    <Publish Dialog=\"MaintenanceWelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\">1</Publish>\n\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RepairButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RemoveButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceWelcomeDlg\">1</Publish>\n\n    <Property Id=\"ARPNOMODIFY\" Value=\"1\" />\n  </UI>\n\n  <UIRef Id=\"WixUI_Common\" />\n</Fragment>\n</Wix>\n`,\n\n\t\"LICENSE.rtf\":           storageBase + \"windows/LICENSE.rtf\",\n\t\"images/Banner.jpg\":     storageBase + \"windows/Banner.jpg\",\n\t\"images/Dialog.jpg\":     storageBase + \"windows/Dialog.jpg\",\n\t\"images/DialogLeft.jpg\": storageBase + \"windows/DialogLeft.jpg\",\n\t\"images/gopher.ico\":     storageBase + \"windows/gopher.ico\",\n}\n\n// runSelfTests contains the tests for this file, since this file\n// is +build ignore. This is called by releaselet_test.go with an\n// environment variable set, which func main above recognizes.\nfunc runSelfTests() {\n\t// Test splitVersion.\n\tfor _, tt := range []struct {\n\t\tv                   string\n\t\tmajor, minor, patch int\n\t}{\n\t\t{\"go1\", 1, 0, 0},\n\t\t{\"go1.34\", 1, 34, 0},\n\t\t{\"go1.34.7\", 1, 34, 7},\n\t} {\n\t\tmajor, minor, patch := splitVersion(tt.v)\n\t\tif major != tt.major || minor != tt.minor || patch != tt.patch {\n\t\t\tlog.Fatalf(\"splitVersion(%q) = %v, %v, %v; want %v, %v, %v\",\n\t\t\t\ttt.v, major, minor, patch, tt.major, tt.minor, tt.patch)\n\t\t}\n\t}\n\n\tfmt.Println(\"ok\")\n}\n"
+const releaselet = "// Copyright 2015 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build ignore\n// +build ignore\n\n// Command releaselet does buildlet-side release construction tasks.\n// It is intended to be executed on the buildlet preparing a release.\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc main() {\n\tif v, _ := strconv.ParseBool(os.Getenv(\"RUN_RELEASELET_TESTS\")); v {\n\t\trunSelfTests()\n\t\treturn\n\t}\n\n\tif dir := archDir(); dir != \"\" {\n\t\tif err := cp(\"go/bin/go\", \"go/bin/\"+dir+\"/go\"); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tif err := cp(\"go/bin/gofmt\", \"go/bin/\"+dir+\"/gofmt\"); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t\tos.RemoveAll(\"go/bin/\" + dir)\n\t\tos.RemoveAll(\"go/pkg/linux_amd64\")\n\t\tos.RemoveAll(\"go/pkg/tool/linux_amd64\")\n\t}\n\tos.RemoveAll(\"go/pkg/obj\")\n\tif runtime.GOOS == \"windows\" {\n\t\t// Clean up .exe~ files; golang.org/issue/23894\n\t\tfilepath.Walk(\"go\", func(path string, fi os.FileInfo, err error) error {\n\t\t\tif strings.HasSuffix(path, \".exe~\") {\n\t\t\t\tos.Remove(path)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\tif err := windowsMSI(); err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\t}\n}\n\nfunc archDir() string {\n\tif os.Getenv(\"GO_BUILDER_NAME\") == \"linux-s390x-crosscompile\" {\n\t\treturn \"linux_s390x\"\n\t}\n\treturn \"\"\n}\n\nfunc environ() (cwd, version string, err error) {\n\tcwd, err = os.Getwd()\n\tif err != nil {\n\t\treturn\n\t}\n\tvar versionBytes []byte\n\tversionBytes, err = ioutil.ReadFile(\"go/VERSION\")\n\tif err != nil {\n\t\treturn\n\t}\n\tversion = string(bytes.TrimSpace(versionBytes))\n\treturn\n}\n\nfunc windowsMSI() error {\n\tcwd, version, err := environ()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Install Wix tools.\n\twix := filepath.Join(cwd, \"wix\")\n\tdefer os.RemoveAll(wix)\n\tswitch runtime.GOARCH {\n\tdefault:\n\t\tif err := installWix(wixRelease311, wix); err != nil {\n\t\t\treturn err\n\t\t}\n\tcase \"arm64\":\n\t\tif err := installWix(wixRelease314, wix); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Write out windows data that is used by the packaging process.\n\twin := filepath.Join(cwd, \"windows\")\n\tdefer os.RemoveAll(win)\n\tif err := writeDataFiles(windowsData, win); err != nil {\n\t\treturn err\n\t}\n\n\t// Gather files.\n\tgoDir := filepath.Join(cwd, \"go\")\n\tappfiles := filepath.Join(win, \"AppFiles.wxs\")\n\tif err := runDir(win, filepath.Join(wix, \"heat\"),\n\t\t\"dir\", goDir,\n\t\t\"-nologo\",\n\t\t\"-gg\", \"-g1\", \"-srd\", \"-sfrag\",\n\t\t\"-cg\", \"AppFiles\",\n\t\t\"-template\", \"fragment\",\n\t\t\"-dr\", \"INSTALLDIR\",\n\t\t\"-var\", \"var.SourceDir\",\n\t\t\"-out\", appfiles,\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tmsArch := func() string {\n\t\tswitch runtime.GOARCH {\n\t\tdefault:\n\t\t\tpanic(\"unknown arch for windows \" + runtime.GOARCH)\n\t\tcase \"386\":\n\t\t\treturn \"x86\"\n\t\tcase \"amd64\":\n\t\t\treturn \"x64\"\n\t\tcase \"arm64\":\n\t\t\treturn \"arm64\"\n\t\t}\n\t}\n\n\t// Build package.\n\tverMajor, verMinor, verPatch := splitVersion(version)\n\n\tif err := runDir(win, filepath.Join(wix, \"candle\"),\n\t\t\"-nologo\",\n\t\t\"-arch\", msArch(),\n\t\t\"-dGoVersion=\"+version,\n\t\tfmt.Sprintf(\"-dWixGoVersion=%v.%v.%v\", verMajor, verMinor, verPatch),\n\t\t\"-dArch=\"+runtime.GOARCH,\n\t\t\"-dSourceDir=\"+goDir,\n\t\tfilepath.Join(win, \"installer.wxs\"),\n\t\tappfiles,\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tmsi := filepath.Join(cwd, \"msi\") // known to cmd/release\n\tif err := os.Mkdir(msi, 0755); err != nil {\n\t\treturn err\n\t}\n\treturn runDir(win, filepath.Join(wix, \"light\"),\n\t\t\"-nologo\",\n\t\t\"-dcl:high\",\n\t\t\"-ext\", \"WixUIExtension\",\n\t\t\"-ext\", \"WixUtilExtension\",\n\t\t\"AppFiles.wixobj\",\n\t\t\"installer.wixobj\",\n\t\t\"-o\", filepath.Join(msi, \"go.msi\"), // file name irrelevant\n\t)\n}\n\ntype wixRelease struct {\n\tBinaryURL string\n\tSHA256    string\n}\n\nvar (\n\twixRelease311 = wixRelease{\n\t\tBinaryURL: \"https://storage.googleapis.com/go-builder-data/wix311-binaries.zip\",\n\t\tSHA256:    \"da034c489bd1dd6d8e1623675bf5e899f32d74d6d8312f8dd125a084543193de\",\n\t}\n\twixRelease314 = wixRelease{\n\t\tBinaryURL: \"https://storage.googleapis.com/go-builder-data/wix314-binaries.zip\",\n\t\tSHA256:    \"34dcbba9952902bfb710161bd45ee2e721ffa878db99f738285a21c9b09c6edb\", // WiX v3.14.0.4118 release, SHA 256 of wix314-binaries.zip from https://wixtoolset.org/releases/v3-14-0-4118/.\n\t}\n)\n\n// installWix fetches and installs the wix toolkit to the specified path.\nfunc installWix(wix wixRelease, path string) error {\n\t// Fetch wix binary zip file.\n\tbody, err := httpGet(wix.BinaryURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Verify sha256.\n\tsum := sha256.Sum256(body)\n\tif fmt.Sprintf(\"%x\", sum) != wix.SHA256 {\n\t\treturn errors.New(\"sha256 mismatch for wix toolkit\")\n\t}\n\n\t// Unzip to path.\n\tzr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, f := range zr.File {\n\t\tname := filepath.FromSlash(f.Name)\n\t\terr := os.MkdirAll(filepath.Join(path, filepath.Dir(name)), 0755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\trc, err := f.Open()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb, err := ioutil.ReadAll(rc)\n\t\trc.Close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = ioutil.WriteFile(filepath.Join(path, name), b, 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc httpGet(url string) ([]byte, error) {\n\tr, err := http.Get(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbody, err := ioutil.ReadAll(r.Body)\n\tr.Body.Close()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif r.StatusCode != 200 {\n\t\treturn nil, errors.New(r.Status)\n\t}\n\treturn body, nil\n}\n\nfunc run(name string, arg ...string) error {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr\n\treturn cmd.Run()\n}\n\nfunc runDir(dir, name string, arg ...string) error {\n\tcmd := exec.Command(name, arg...)\n\tcmd.Dir = dir\n\tif dir != \"\" {\n\t\tcmd.Env = append(os.Environ(), \"PWD=\"+dir)\n\t}\n\tcmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr\n\treturn cmd.Run()\n}\n\nfunc cp(dst, src string) error {\n\tsf, err := os.Open(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sf.Close()\n\tfi, err := sf.Stat()\n\tif err != nil {\n\t\treturn err\n\t}\n\ttmpDst := dst + \".tmp\"\n\tdf, err := os.Create(tmpDst)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer df.Close()\n\t// Windows doesn't implement Fchmod.\n\tif runtime.GOOS != \"windows\" {\n\t\tif err := df.Chmod(fi.Mode()); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err = io.Copy(df, sf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := df.Close(); err != nil {\n\t\treturn err\n\t}\n\tif err := os.Rename(tmpDst, dst); err != nil {\n\t\treturn err\n\t}\n\t// Ensure the destination has the same mtime as the source.\n\treturn os.Chtimes(dst, fi.ModTime(), fi.ModTime())\n}\n\nfunc cpDir(dst, src string) error {\n\twalk := func(srcPath string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdstPath := filepath.Join(dst, srcPath[len(src):])\n\t\tif info.IsDir() {\n\t\t\treturn os.MkdirAll(dstPath, 0755)\n\t\t}\n\t\treturn cp(dstPath, srcPath)\n\t}\n\treturn filepath.Walk(src, walk)\n}\n\nfunc cpAllDir(dst, basePath string, dirs ...string) error {\n\tfor _, dir := range dirs {\n\t\tif err := cpDir(filepath.Join(dst, dir), filepath.Join(basePath, dir)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ext() string {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \".exe\"\n\t}\n\treturn \"\"\n}\n\nvar versionRe = regexp.MustCompile(`^go(\\d+(\\.\\d+)*)`)\n\n// splitVersion splits a Go version string such as \"go1.9\" or \"go1.10.2\" (as matched by versionRe)\n// into its three parts: major, minor, and patch\n// It's based on the Git tag.\nfunc splitVersion(v string) (major, minor, patch int) {\n\tm := versionRe.FindStringSubmatch(v)\n\tif m == nil {\n\t\treturn\n\t}\n\tparts := strings.Split(m[1], \".\")\n\tif len(parts) >= 1 {\n\t\tmajor, _ = strconv.Atoi(parts[0])\n\n\t\tif len(parts) >= 2 {\n\t\t\tminor, _ = strconv.Atoi(parts[1])\n\n\t\t\tif len(parts) >= 3 {\n\t\t\t\tpatch, _ = strconv.Atoi(parts[2])\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nconst storageBase = \"https://storage.googleapis.com/go-builder-data/release/\"\n\n// writeDataFiles writes the files in the provided map to the provided base\n// directory. If the map value is a URL it fetches the data at that URL and\n// uses it as the file contents.\nfunc writeDataFiles(data map[string]string, base string) error {\n\tfor name, body := range data {\n\t\tdst := filepath.Join(base, name)\n\t\terr := os.MkdirAll(filepath.Dir(dst), 0755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tb := []byte(body)\n\t\tif strings.HasPrefix(body, storageBase) {\n\t\t\tb, err = httpGet(body)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\t// (We really mean 0755 on the next line; some of these files\n\t\t// are executable, and there's no harm in making them all so.)\n\t\tif err := ioutil.WriteFile(dst, b, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nvar windowsData = map[string]string{\n\n\t\"installer.wxs\": `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n<!--\n# Copyright 2010 The Go Authors. All rights reserved.\n# Use of this source code is governed by a BSD-style\n# license that can be found in the LICENSE file.\n-->\n\n<?if $(var.Arch) = 386 ?>\n  <?define UpgradeCode = {1C3114EA-08C3-11E1-9095-7FCA4824019B} ?>\n  <?define InstallerVersion=\"300\" ?>\n  <?define SysFolder=SystemFolder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFilesFolder\" ?>\n<?elseif $(var.Arch) = arm64 ?>\n  <?define UpgradeCode = {21ade9a3-3fdd-4ba6-bea6-c85abadc9488} ?>\n  <?define InstallerVersion=\"500\" ?>\n  <?define SysFolder=System64Folder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFiles64Folder\" ?>\n<?else?>\n  <?define UpgradeCode = {22ea7650-4ac6-4001-bf29-f4b8775db1c0} ?>\n  <?define InstallerVersion=\"300\" ?>\n  <?define SysFolder=System64Folder ?>\n  <?define ArchProgramFilesFolder=\"ProgramFiles64Folder\" ?>\n<?endif?>\n\n<Product\n    Id=\"*\"\n    Name=\"Go Programming Language $(var.Arch) $(var.GoVersion)\"\n    Language=\"1033\"\n    Version=\"$(var.WixGoVersion)\"\n    Manufacturer=\"https://golang.org\"\n    UpgradeCode=\"$(var.UpgradeCode)\" >\n\n<Package\n    Id='*'\n    Keywords='Installer'\n    Description=\"The Go Programming Language Installer\"\n    Comments=\"The Go programming language is an open source project to make programmers more productive.\"\n    InstallerVersion=\"$(var.InstallerVersion)\"\n    Compressed=\"yes\"\n    InstallScope=\"perMachine\"\n    Languages=\"1033\" />\n\n<Property Id=\"ARPCOMMENTS\" Value=\"The Go programming language is a fast, statically typed, compiled language that feels like a dynamically typed, interpreted language.\" />\n<Property Id=\"ARPCONTACT\" Value=\"golang-nuts@googlegroups.com\" />\n<Property Id=\"ARPHELPLINK\" Value=\"https://golang.org/help/\" />\n<Property Id=\"ARPREADME\" Value=\"https://golang.org\" />\n<Property Id=\"ARPURLINFOABOUT\" Value=\"https://golang.org\" />\n<Property Id=\"LicenseAccepted\">1</Property>\n<Icon Id=\"gopher.ico\" SourceFile=\"images\\gopher.ico\"/>\n<Property Id=\"ARPPRODUCTICON\" Value=\"gopher.ico\" />\n<Property Id=\"EXISTING_GOLANG_INSTALLED\">\n  <RegistrySearch Id=\"installed\" Type=\"raw\" Root=\"HKCU\" Key=\"Software\\GoProgrammingLanguage\" Name=\"installed\" />\n</Property>\n<Media Id='1' Cabinet=\"go.cab\" EmbedCab=\"yes\" CompressionLevel=\"high\" />\n<Condition Message=\"Windows 7 (with Service Pack 1) or greater required.\">\n    ((VersionNT > 601) OR (VersionNT = 601 AND ServicePackLevel >= 1))\n</Condition>\n<MajorUpgrade AllowDowngrades=\"yes\" />\n\n<CustomAction\n    Id=\"SetApplicationRootDirectory\"\n    Property=\"ARPINSTALLLOCATION\"\n    Value=\"[INSTALLDIR]\" />\n\n<!-- Define the directory structure and environment variables -->\n<Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n  <Directory Id=\"$(var.ArchProgramFilesFolder)\">\n    <Directory Id=\"INSTALLDIR\" Name=\"Go\"/>\n  </Directory>\n  <Directory Id=\"ProgramMenuFolder\">\n    <Directory Id=\"GoProgramShortcutsDir\" Name=\"Go Programming Language\"/>\n  </Directory>\n  <Directory Id=\"EnvironmentEntries\">\n    <Directory Id=\"GoEnvironmentEntries\" Name=\"Go Programming Language\"/>\n  </Directory>\n</Directory>\n\n<!-- Programs Menu Shortcuts -->\n<DirectoryRef Id=\"GoProgramShortcutsDir\">\n  <Component Id=\"Component_GoProgramShortCuts\" Guid=\"{f5fbfb5e-6c5c-423b-9298-21b0e3c98f4b}\">\n    <Shortcut\n        Id=\"UninstallShortcut\"\n        Name=\"Uninstall Go\"\n        Description=\"Uninstalls Go and all of its components\"\n        Target=\"[$(var.SysFolder)]msiexec.exe\"\n        Arguments=\"/x [ProductCode]\" />\n    <RemoveFolder\n        Id=\"GoProgramShortcutsDir\"\n        On=\"uninstall\" />\n    <RegistryValue\n        Root=\"HKCU\"\n        Key=\"Software\\GoProgrammingLanguage\"\n        Name=\"ShortCuts\"\n        Type=\"integer\"\n        Value=\"1\"\n        KeyPath=\"yes\" />\n  </Component>\n</DirectoryRef>\n\n<!-- Registry & Environment Settings -->\n<DirectoryRef Id=\"GoEnvironmentEntries\">\n  <Component Id=\"Component_GoEnvironment\" Guid=\"{3ec7a4d5-eb08-4de7-9312-2df392c45993}\">\n    <RegistryKey\n        Root=\"HKCU\"\n        Key=\"Software\\GoProgrammingLanguage\">\n            <RegistryValue\n                Name=\"installed\"\n                Type=\"integer\"\n                Value=\"1\"\n                KeyPath=\"yes\" />\n            <RegistryValue\n                Name=\"installLocation\"\n                Type=\"string\"\n                Value=\"[INSTALLDIR]\" />\n    </RegistryKey>\n    <Environment\n        Id=\"GoPathEntry\"\n        Action=\"set\"\n        Part=\"last\"\n        Name=\"PATH\"\n        Permanent=\"no\"\n        System=\"yes\"\n        Value=\"[INSTALLDIR]bin\" />\n    <Environment\n        Id=\"UserGoPath\"\n        Action=\"create\"\n        Name=\"GOPATH\"\n        Permanent=\"no\"\n        Value=\"%USERPROFILE%\\go\" />\n    <Environment\n        Id=\"UserGoPathEntry\"\n        Action=\"set\"\n        Part=\"last\"\n        Name=\"PATH\"\n        Permanent=\"no\"\n        Value=\"%USERPROFILE%\\go\\bin\" />\n    <RemoveFolder\n        Id=\"GoEnvironmentEntries\"\n        On=\"uninstall\" />\n  </Component>\n</DirectoryRef>\n\n<!-- Install the files -->\n<Feature\n    Id=\"GoTools\"\n    Title=\"Go\"\n    Level=\"1\">\n      <ComponentRef Id=\"Component_GoEnvironment\" />\n      <ComponentGroupRef Id=\"AppFiles\" />\n      <ComponentRef Id=\"Component_GoProgramShortCuts\" />\n</Feature>\n\n<!-- Update the environment -->\n<InstallExecuteSequence>\n    <Custom Action=\"SetApplicationRootDirectory\" Before=\"InstallFinalize\" />\n</InstallExecuteSequence>\n\n<!-- Notify top level applications of the new PATH variable (golang.org/issue/18680)  -->\n<CustomActionRef Id=\"WixBroadcastEnvironmentChange\" />\n\n<!-- Include the user interface -->\n<WixVariable Id=\"WixUILicenseRtf\" Value=\"LICENSE.rtf\" />\n<WixVariable Id=\"WixUIBannerBmp\" Value=\"images\\Banner.jpg\" />\n<WixVariable Id=\"WixUIDialogBmp\" Value=\"images\\Dialog.jpg\" />\n<Property Id=\"WIXUI_INSTALLDIR\" Value=\"INSTALLDIR\" />\n<UIRef Id=\"Golang_InstallDir\" />\n<UIRef Id=\"WixUI_ErrorProgressText\" />\n\n</Product>\n<Fragment>\n  <!--\n    The installer steps are modified so we can get user confirmation to uninstall an existing golang installation.\n\n    WelcomeDlg  [not installed]  =>                  LicenseAgreementDlg => InstallDirDlg  ..\n                [installed]      => OldVersionDlg => LicenseAgreementDlg => InstallDirDlg  ..\n  -->\n  <UI Id=\"Golang_InstallDir\">\n    <!-- style -->\n    <TextStyle Id=\"WixUI_Font_Normal\" FaceName=\"Tahoma\" Size=\"8\" />\n    <TextStyle Id=\"WixUI_Font_Bigger\" FaceName=\"Tahoma\" Size=\"12\" />\n    <TextStyle Id=\"WixUI_Font_Title\" FaceName=\"Tahoma\" Size=\"9\" Bold=\"yes\" />\n\n    <Property Id=\"DefaultUIFont\" Value=\"WixUI_Font_Normal\" />\n    <Property Id=\"WixUI_Mode\" Value=\"InstallDir\" />\n\n    <!-- dialogs -->\n    <DialogRef Id=\"BrowseDlg\" />\n    <DialogRef Id=\"DiskCostDlg\" />\n    <DialogRef Id=\"ErrorDlg\" />\n    <DialogRef Id=\"FatalError\" />\n    <DialogRef Id=\"FilesInUse\" />\n    <DialogRef Id=\"MsiRMFilesInUse\" />\n    <DialogRef Id=\"PrepareDlg\" />\n    <DialogRef Id=\"ProgressDlg\" />\n    <DialogRef Id=\"ResumeDlg\" />\n    <DialogRef Id=\"UserExit\" />\n    <Dialog Id=\"OldVersionDlg\" Width=\"240\" Height=\"95\" Title=\"[ProductName] Setup\" NoMinimize=\"yes\">\n      <Control Id=\"Text\" Type=\"Text\" X=\"28\" Y=\"15\" Width=\"194\" Height=\"50\">\n        <Text>A previous version of Go Programming Language is currently installed. By continuing the installation this version will be uninstalled. Do you want to continue?</Text>\n      </Control>\n      <Control Id=\"Exit\" Type=\"PushButton\" X=\"123\" Y=\"67\" Width=\"62\" Height=\"17\"\n        Default=\"yes\" Cancel=\"yes\" Text=\"No, Exit\">\n        <Publish Event=\"EndDialog\" Value=\"Exit\">1</Publish>\n      </Control>\n      <Control Id=\"Next\" Type=\"PushButton\" X=\"55\" Y=\"67\" Width=\"62\" Height=\"17\" Text=\"Yes, Uninstall\">\n        <Publish Event=\"EndDialog\" Value=\"Return\">1</Publish>\n      </Control>\n    </Dialog>\n\n    <!-- wizard steps -->\n    <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"3\">1</Publish>\n    <Publish Dialog=\"BrowseDlg\" Control=\"OK\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"4\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n\n    <Publish Dialog=\"ExitDialog\" Control=\"Finish\" Event=\"EndDialog\" Value=\"Return\" Order=\"999\">1</Publish>\n\n    <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"OldVersionDlg\"><![CDATA[EXISTING_GOLANG_INSTALLED << \"#1\"]]> </Publish>\n    <Publish Dialog=\"WelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\"><![CDATA[NOT (EXISTING_GOLANG_INSTALLED << \"#1\")]]></Publish>\n\n    <Publish Dialog=\"OldVersionDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">1</Publish>\n\n    <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\">1</Publish>\n    <Publish Dialog=\"LicenseAgreementDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"InstallDirDlg\">LicenseAccepted = \"1\"</Publish>\n\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"LicenseAgreementDlg\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SetTargetPath\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"DoAction\" Value=\"WixUIValidatePath\" Order=\"2\">NOT WIXUI_DONTVALIDATEPATH</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"SpawnDialog\" Value=\"InvalidDirDlg\" Order=\"3\"><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>\"1\"]]></Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\" Order=\"4\">WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=\"1\"</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Property=\"_BrowseProperty\" Value=\"[WIXUI_INSTALLDIR]\" Order=\"1\">1</Publish>\n    <Publish Dialog=\"InstallDirDlg\" Control=\"ChangeFolder\" Event=\"SpawnDialog\" Value=\"BrowseDlg\" Order=\"2\">1</Publish>\n\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"InstallDirDlg\" Order=\"1\">NOT Installed</Publish>\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\" Order=\"2\">Installed AND NOT PATCH</Publish>\n    <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"WelcomeDlg\" Order=\"2\">Installed AND PATCH</Publish>\n\n    <Publish Dialog=\"MaintenanceWelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\">1</Publish>\n\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RepairButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RemoveButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n    <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceWelcomeDlg\">1</Publish>\n\n    <Property Id=\"ARPNOMODIFY\" Value=\"1\" />\n  </UI>\n\n  <UIRef Id=\"WixUI_Common\" />\n</Fragment>\n</Wix>\n`,\n\n\t\"LICENSE.rtf\":           storageBase + \"windows/LICENSE.rtf\",\n\t\"images/Banner.jpg\":     storageBase + \"windows/Banner.jpg\",\n\t\"images/Dialog.jpg\":     storageBase + \"windows/Dialog.jpg\",\n\t\"images/DialogLeft.jpg\": storageBase + \"windows/DialogLeft.jpg\",\n\t\"images/gopher.ico\":     storageBase + \"windows/gopher.ico\",\n}\n\n// runSelfTests contains the tests for this file, since this file\n// is +build ignore. This is called by releaselet_test.go with an\n// environment variable set, which func main above recognizes.\nfunc runSelfTests() {\n\t// Test splitVersion.\n\tfor _, tt := range []struct {\n\t\tv                   string\n\t\tmajor, minor, patch int\n\t}{\n\t\t{\"go1\", 1, 0, 0},\n\t\t{\"go1.34\", 1, 34, 0},\n\t\t{\"go1.34.7\", 1, 34, 7},\n\t} {\n\t\tmajor, minor, patch := splitVersion(tt.v)\n\t\tif major != tt.major || minor != tt.minor || patch != tt.patch {\n\t\t\tlog.Fatalf(\"splitVersion(%q) = %v, %v, %v; want %v, %v, %v\",\n\t\t\t\ttt.v, major, minor, patch, tt.major, tt.minor, tt.patch)\n\t\t}\n\t}\n\n\tfmt.Println(\"ok\")\n}\n"
diff --git a/cmd/releasebot/main.go b/cmd/releasebot/main.go
index e0718f4..64e790a 100644
--- a/cmd/releasebot/main.go
+++ b/cmd/releasebot/main.go
@@ -28,6 +28,7 @@
 	"time"
 
 	"golang.org/x/build/buildenv"
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/internal/task"
 	"golang.org/x/build/maintner"
 )
@@ -395,7 +396,7 @@
 // runs of side-effect-free commands like "git cat-file commit HEAD".
 func (r *runner) runOut(args ...string) []byte {
 	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Dir = r.dir
+	envutil.SetDir(cmd, r.dir)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		r.w.log.Printf("$ %s\n", strings.Join(args, " "))
@@ -410,10 +411,8 @@
 func (r *runner) runErr(args ...string) ([]byte, error) {
 	r.w.log.Printf("$ %s\n", strings.Join(args, " "))
 	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Dir = r.dir
-	if len(r.extraEnv) > 0 {
-		cmd.Env = append(os.Environ(), r.extraEnv...)
-	}
+	envutil.SetDir(cmd, r.dir)
+	envutil.SetEnv(cmd, r.extraEnv...)
 	return cmd.CombinedOutput()
 }
 
diff --git a/cmd/updatecontrib/updatecontrib.go b/cmd/updatecontrib/updatecontrib.go
index 15f2d2b..eae25f6 100644
--- a/cmd/updatecontrib/updatecontrib.go
+++ b/cmd/updatecontrib/updatecontrib.go
@@ -25,6 +25,7 @@
 	"sort"
 	"strings"
 
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/text/collate"
 	"golang.org/x/text/language"
 )
@@ -326,7 +327,7 @@
 			}
 		}
 		cmd := exec.Command("git", "fetch")
-		cmd.Dir = dir
+		envutil.SetDir(cmd, dir)
 		if out, err := cmd.CombinedOutput(); err != nil {
 			log.Fatalf("Error updating repo %q: %v, %s", repo, err, out)
 		}
@@ -337,7 +338,7 @@
 		}
 
 		cmd = exec.Command("git", "log", "--format=%ae/%h/%an", "origin/master") //, "HEAD@{5 years ago}..HEAD")
-		cmd.Dir = dir
+		envutil.SetDir(cmd, dir)
 		cmd.Stderr = os.Stderr
 		out, err := cmd.StdoutPipe()
 		if err != nil {
diff --git a/cmd/updatestd/updatestd.go b/cmd/updatestd/updatestd.go
index 712796d..6ed4e80 100644
--- a/cmd/updatestd/updatestd.go
+++ b/cmd/updatestd/updatestd.go
@@ -26,6 +26,7 @@
 	"strings"
 
 	"golang.org/x/build/gerrit"
+	"golang.org/x/build/internal/envutil"
 )
 
 func main() {
@@ -256,7 +257,7 @@
 func (r runner) run(args ...string) {
 	log.Printf("> %s\n", strings.Join(args, " "))
 	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Dir = r.dir
+	envutil.SetDir(cmd, r.dir)
 	out, err := cmd.CombinedOutput()
 	if err != nil {
 		log.Fatalf("command failed: %s\n%s", err, out)
@@ -270,7 +271,7 @@
 // and returns the command's standard output.
 func (r runner) runOut(args ...string) []byte {
 	cmd := exec.Command(args[0], args[1:]...)
-	cmd.Dir = r.dir
+	envutil.SetDir(cmd, r.dir)
 	out, err := cmd.Output()
 	if err != nil {
 		log.Printf("> %s\n", strings.Join(args, " "))
diff --git a/cmd/upload/upload.go b/cmd/upload/upload.go
index f1eaaee..9af4618 100644
--- a/cmd/upload/upload.go
+++ b/cmd/upload/upload.go
@@ -22,12 +22,11 @@
 	"os"
 	"os/exec"
 	"regexp"
-	"runtime"
 	"strings"
 	"time"
 
 	"cloud.google.com/go/storage"
-	"golang.org/x/build/envutil"
+	"golang.org/x/build/internal/envutil"
 )
 
 var (
@@ -208,18 +207,18 @@
 	}
 
 	goBin := goCmd()
-	env := append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch)
+	env := []string{"GOOS=" + goos, "GOARCH=" + goarch}
 	if *extraEnv != "" {
 		env = append(env, strings.Split(*extraEnv, ",")...)
 	}
-	env = envutil.Dedup(runtime.GOOS == "windows", env)
+
 	cmd := exec.Command(goBin,
 		"list",
 		"--tags="+*tags,
 		"--installsuffix="+*installSuffix,
 		"-f", "{{.Target}}",
 		target)
-	cmd.Env = env
+	envutil.SetEnv(cmd, env...)
 	out, err := cmd.Output()
 	if err != nil {
 		log.Fatalf("go list: %v", err)
@@ -249,7 +248,7 @@
 	if *verbose {
 		cmd.Stdout = os.Stdout
 	}
-	cmd.Env = env
+	envutil.SetEnv(cmd, env...)
 	if err := cmd.Run(); err != nil {
 		log.Fatalf("go install %s: %v, %s", target, err, stderr.Bytes())
 	}
diff --git a/cmd/xb/xb.go b/cmd/xb/xb.go
index 93261c6..586301b 100644
--- a/cmd/xb/xb.go
+++ b/cmd/xb/xb.go
@@ -29,6 +29,7 @@
 	"strings"
 
 	"golang.org/x/build/buildenv"
+	"golang.org/x/build/internal/envutil"
 )
 
 var (
@@ -198,7 +199,7 @@
 	}
 
 	cmd := exec.Command("make", "docker")
-	cmd.Dir = strings.TrimSpace(string(dir))
+	envutil.SetDir(cmd, strings.TrimSpace(string(dir)))
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	if err := cmd.Run(); err != nil {
diff --git a/envutil/README.md b/envutil/README.md
deleted file mode 100644
index 6f43957..0000000
--- a/envutil/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-<!-- Auto-generated by x/build/update-readmes.go -->
-
-[![Go Reference](https://pkg.go.dev/badge/golang.org/x/build/envutil.svg)](https://pkg.go.dev/golang.org/x/build/envutil)
-
-# golang.org/x/build/envutil
-
-Package envutil provides utilities for working with environment variables.
diff --git a/envutil/dedup.go b/envutil/dedup.go
deleted file mode 100644
index 067ff94..0000000
--- a/envutil/dedup.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2015 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 envutil provides utilities for working with environment variables.
-package envutil
-
-import "strings"
-
-// Dedup returns a copy of env with any duplicates removed, in favor of
-// later values.
-// Items are expected to be on the normal environment "key=value" form.
-// If caseInsensitive is true, the case of keys is ignored.
-func Dedup(caseInsensitive bool, env []string) []string {
-	out := make([]string, 0, len(env))
-	saw := map[string]int{} // to index in the array
-	for _, kv := range env {
-		eq := strings.Index(kv, "=")
-		if eq < 1 {
-			out = append(out, kv)
-			continue
-		}
-		k := kv[:eq]
-		if caseInsensitive {
-			k = strings.ToLower(k)
-		}
-		if dupIdx, isDup := saw[k]; isDup {
-			out[dupIdx] = kv
-		} else {
-			saw[k] = len(out)
-			out = append(out, kv)
-		}
-	}
-	return out
-}
diff --git a/envutil/dedup_test.go b/envutil/dedup_test.go
deleted file mode 100644
index 50621e1..0000000
--- a/envutil/dedup_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2015 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 envutil
-
-import (
-	"reflect"
-	"testing"
-)
-
-func TestDedup(t *testing.T) {
-	tests := []struct {
-		noCase bool
-		in     []string
-		want   []string
-	}{
-		{
-			noCase: true,
-			in:     []string{"k1=v1", "k2=v2", "K1=v3"},
-			want:   []string{"K1=v3", "k2=v2"},
-		},
-		{
-			noCase: false,
-			in:     []string{"k1=v1", "K1=V2", "k1=v3"},
-			want:   []string{"k1=v3", "K1=V2"},
-		},
-	}
-	for _, tt := range tests {
-		got := Dedup(tt.noCase, tt.in)
-		if !reflect.DeepEqual(got, tt.want) {
-			t.Errorf("Dedup(%v, %q) = %q; want %q", tt.noCase, tt.in, got, tt.want)
-		}
-	}
-}
diff --git a/internal/envutil/envutil.go b/internal/envutil/envutil.go
new file mode 100644
index 0000000..e2a7906
--- /dev/null
+++ b/internal/envutil/envutil.go
@@ -0,0 +1,125 @@
+// Copyright 2015 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 envutil provides utilities for working with environment variables.
+package envutil
+
+import (
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+)
+
+// Dedup returns a copy of env with any duplicates removed, in favor of
+// later values.
+// Items are expected to be on the normal environment "key=value" form.
+//
+// Keys are interpreted as if on the given GOOS.
+// (On Windows, key comparison is case-insensitive.)
+func Dedup(goos string, env []string) []string {
+	caseInsensitive := (goos == "windows")
+
+	// Construct the output in reverse order, to preserve the
+	// last occurrence of each key.
+	saw := map[string]bool{}
+	out := make([]string, 0, len(env))
+	for n := len(env); n > 0; n-- {
+		kv := env[n-1]
+
+		k, _ := Split(kv)
+		if caseInsensitive {
+			k = strings.ToLower(k)
+		}
+		if saw[k] {
+			continue
+		}
+
+		saw[k] = true
+		out = append(out, kv)
+	}
+
+	// Now reverse the slice to restore the original order.
+	for i := 0; i < len(out)/2; i++ {
+		j := len(out) - i - 1
+		out[i], out[j] = out[j], out[i]
+	}
+
+	return out
+}
+
+// Get returns the value of key in env, interpreted according to goos.
+func Get(goos string, env []string, key string) string {
+	for n := len(env); n > 0; n-- {
+		kv := env[n-1]
+		if v, ok := Match(goos, kv, key); ok {
+			return v
+		}
+	}
+	return ""
+}
+
+// Match checks whether a "key=value" string matches key and, if so,
+// returns the value.
+//
+// On Windows, the key comparison is case-insensitive.
+func Match(goos, kv, key string) (value string, ok bool) {
+	if len(kv) <= len(key) || kv[len(key)] != '=' {
+		return "", false
+	}
+
+	if goos == "windows" {
+		// Case insensitive.
+		if !strings.EqualFold(kv[:len(key)], key) {
+			return "", false
+		}
+	} else {
+		// Case sensitive.
+		if kv[:len(key)] != key {
+			return "", false
+		}
+	}
+
+	return kv[len(key)+1:], true
+}
+
+// Split splits a "key=value" string into a key and value.
+func Split(kv string) (key, value string) {
+	parts := strings.SplitN(kv, "=", 2)
+	if len(parts) < 2 {
+		return parts[0], ""
+	}
+	return parts[0], parts[1]
+}
+
+// SetDir sets cmd.Dir to dir, and also updates cmd.Env to ensure that PWD matches.
+//
+// If dir is the empty string, SetDir clears cmd.Dir and sets PWD to the current
+// working directory.
+func SetDir(cmd *exec.Cmd, dir string) {
+	if dir == "" {
+		cmd.Dir = ""
+		dir, _ = os.Getwd()
+	} else {
+		cmd.Dir = dir
+	}
+	SetEnv(cmd, "PWD="+dir)
+}
+
+// SetEnv sets cmd.Env to include the given key=value pairs,
+// removing any duplicates for the key and leaving all other keys unchanged.
+//
+// (Removing duplicates is not strictly necessary with modern versions of the Go
+// standard library, but causes less confusion if cmd.Env is written to a log —
+// as is sometimes done in packages within this module.)
+func SetEnv(cmd *exec.Cmd, kv ...string) {
+	if len(kv) == 0 {
+		return
+	}
+	env := cmd.Env
+	if env == nil {
+		env = os.Environ()
+	}
+	cmd.Env = Dedup(runtime.GOOS, append(env, kv...))
+}
diff --git a/internal/envutil/envutil_test.go b/internal/envutil/envutil_test.go
new file mode 100644
index 0000000..b89d06f
--- /dev/null
+++ b/internal/envutil/envutil_test.go
@@ -0,0 +1,82 @@
+// Copyright 2015 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 envutil
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestDedup(t *testing.T) {
+	tests := []struct {
+		in   []string
+		want map[string][]string // keyed by GOOS
+	}{
+		{
+			in: []string{"k1=v1", "k2=v2", "K1=v3"},
+			want: map[string][]string{
+				"windows": {"k2=v2", "K1=v3"},
+				"linux":   {"k1=v1", "k2=v2", "K1=v3"},
+			},
+		},
+		{
+			in: []string{"k1=v1", "K1=V2", "k1=v3"},
+			want: map[string][]string{
+				"windows": {"k1=v3"},
+				"linux":   {"K1=V2", "k1=v3"},
+			},
+		},
+	}
+	for i, tt := range tests {
+		t.Run(fmt.Sprint(i), func(t *testing.T) {
+			for goos, want := range tt.want {
+				t.Run(goos, func(t *testing.T) {
+					got := Dedup(goos, tt.in)
+					if !reflect.DeepEqual(got, want) {
+						t.Errorf("Dedup(%q, %q) = %q; want %q", goos, tt.in, got, want)
+					}
+				})
+			}
+		})
+	}
+}
+
+func TestGet(t *testing.T) {
+	tests := []struct {
+		env  []string
+		want map[string]map[string]string // GOOS → key → value
+	}{
+		{
+			env: []string{"k1=v1", "k2=v2", "K1=v3"},
+			want: map[string]map[string]string{
+				"windows": {"k1": "v3", "k2": "v2", "K1": "v3", "K2": "v2"},
+				"linux":   {"k1": "v1", "k2": "v2", "K1": "v3", "K2": ""},
+			},
+		},
+		{
+			env: []string{"k1=v1", "K1=V2", "k1=v3"},
+			want: map[string]map[string]string{
+				"windows": {"k1": "v3", "K1": "v3"},
+				"linux":   {"k1": "v3", "K1": "V2"},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprint(i), func(t *testing.T) {
+			for goos, m := range tt.want {
+				t.Run(goos, func(t *testing.T) {
+					for k, want := range m {
+						got := Get(goos, tt.env, k)
+						if got != want {
+							t.Errorf("Get(%q, %q, %q) = %q; want %q", goos, tt.env, k, got, want)
+						}
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/maintner/gerrit.go b/maintner/gerrit.go
index e9007f5..cb29b15 100644
--- a/maintner/gerrit.go
+++ b/maintner/gerrit.go
@@ -26,6 +26,7 @@
 	"strings"
 	"time"
 
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/maintner/maintpb"
 )
 
@@ -1035,9 +1036,9 @@
 
 	t0 := time.Now()
 	cmd := exec.CommandContext(ctx, "git", "fetch", "origin")
-	cmd.Dir = gitDir
+	envutil.SetDir(cmd, gitDir)
 	// Enable extra Git tracing in case the fetch hangs.
-	cmd.Env = append(os.Environ(),
+	envutil.SetEnv(cmd,
 		"GIT_TRACE2_EVENT=1",
 		"GIT_TRACE_CURL_NO_DATA=1",
 	)
@@ -1066,7 +1067,7 @@
 
 	t0 = time.Now()
 	cmd = exec.CommandContext(ctx, "git", "ls-remote")
-	cmd.Dir = gitDir
+	envutil.SetDir(cmd, gitDir)
 	out, err := cmd.CombinedOutput()
 	lsRemoteDuration := time.Since(t0).Round(time.Millisecond)
 	if err != nil {
@@ -1232,7 +1233,7 @@
 	gp.logf("fetching %v hashes...", len(hashes))
 	t0 := time.Now()
 	cmd := exec.CommandContext(ctx, "git", args...)
-	cmd.Dir = gp.gitDir()
+	envutil.SetDir(cmd, gp.gitDir())
 	out, err := cmd.CombinedOutput()
 	d := time.Since(t0).Round(time.Millisecond)
 	if err != nil {
@@ -1263,7 +1264,7 @@
 
 	if _, err := os.Stat(filepath.Join(gitDir, ".git", "config")); err == nil {
 		cmd := exec.CommandContext(ctx, "git", "remote", "-v")
-		cmd.Dir = gitDir
+		envutil.SetDir(cmd, gitDir)
 		remoteBytes, err := cmd.Output()
 		if err != nil {
 			return fmt.Errorf("running git remote -v in %v: %v", gitDir, formatExecError(err))
@@ -1279,7 +1280,7 @@
 	buf := new(bytes.Buffer)
 	cmd.Stdout = buf
 	cmd.Stderr = buf
-	cmd.Dir = gitDir
+	envutil.SetDir(cmd, gitDir)
 	if err := cmd.Run(); err != nil {
 		log.Printf(`Error running "git init": %s`, buf.String())
 		return err
@@ -1288,7 +1289,7 @@
 	cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", "https://"+gp.proj)
 	cmd.Stdout = buf
 	cmd.Stderr = buf
-	cmd.Dir = gitDir
+	envutil.SetDir(cmd, gitDir)
 	if err := cmd.Run(); err != nil {
 		log.Printf(`Error running "git remote add origin": %s`, buf.String())
 		return err
diff --git a/maintner/git.go b/maintner/git.go
index 8c6a2ed..6e9299f 100644
--- a/maintner/git.go
+++ b/maintner/git.go
@@ -18,6 +18,7 @@
 	"strings"
 	"time"
 
+	"golang.org/x/build/internal/envutil"
 	"golang.org/x/build/internal/foreach"
 	"golang.org/x/build/maintner/maintpb"
 )
@@ -179,7 +180,7 @@
 // syncGitCommits polls for git commits in a directory.
 func (c *Corpus) syncGitCommits(ctx context.Context, conf polledGitCommits, loop bool) error {
 	cmd := exec.CommandContext(ctx, "git", "show-ref", "refs/remotes/origin/master")
-	cmd.Dir = conf.dir
+	envutil.SetDir(cmd, conf.dir)
 	out, err := cmd.Output()
 	if err != nil {
 		log.Fatal(err)
@@ -248,13 +249,13 @@
 
 func parseCommitFromGit(dir string, hash GitHash) (*maintpb.GitCommit, error) {
 	cmd := exec.Command("git", "cat-file", "commit", hash.String())
-	cmd.Dir = dir
+	envutil.SetDir(cmd, dir)
 	catFile, err := cmd.Output()
 	if err != nil {
 		return nil, fmt.Errorf("git cat-file -p %v: %v", hash, err)
 	}
 	cmd = exec.Command("git", "diff-tree", "--numstat", hash.String())
-	cmd.Dir = dir
+	envutil.SetDir(cmd, dir)
 	diffTreeOut, err := cmd.Output()
 	if err != nil {
 		return nil, fmt.Errorf("git diff-tree --numstat %v: %v", hash, err)
diff --git a/version/version.go b/version/version.go
index 7f6dc5d..c64b28b 100644
--- a/version/version.go
+++ b/version/version.go
@@ -19,16 +19,177 @@
 	"net/http"
 	"os"
 	"os/exec"
+	"os/signal"
 	"os/user"
 	"path"
 	"path/filepath"
+	"regexp"
 	"runtime"
+	"strconv"
 	"strings"
+	"syscall"
 	"time"
-
-	"golang.org/x/build/envutil"
 )
 
+// RunTip runs the "go" tool from the development tree.
+func RunTip() {
+	log.SetFlags(0)
+
+	root, err := goroot("gotip")
+	if err != nil {
+		log.Fatalf("gotip: %v", err)
+	}
+
+	if len(os.Args) > 1 && os.Args[1] == "download" {
+		switch len(os.Args) {
+		case 2:
+			if err := installTip(root, ""); err != nil {
+				log.Fatalf("gotip: %v", err)
+			}
+		case 3:
+			if err := installTip(root, os.Args[2]); err != nil {
+				log.Fatalf("gotip: %v", err)
+			}
+		default:
+			log.Fatalf("gotip: usage: gotip download [CL number | branch name]")
+		}
+		log.Printf("Success. You may now run 'gotip'!")
+		os.Exit(0)
+	}
+
+	gobin := filepath.Join(root, "bin", "go"+exe())
+	if _, err := os.Stat(gobin); err != nil {
+		log.Fatalf("gotip: not downloaded. Run 'gotip download' to install to %v", root)
+	}
+
+	runGo(root)
+}
+
+func installTip(root, target string) error {
+	git := func(args ...string) error {
+		cmd := exec.Command("git", args...)
+		cmd.Stdin = os.Stdin
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		cmd.Dir = root
+		return cmd.Run()
+	}
+	gitOutput := func(args ...string) ([]byte, error) {
+		cmd := exec.Command("git", args...)
+		cmd.Dir = root
+		return cmd.Output()
+	}
+
+	if _, err := os.Stat(filepath.Join(root, ".git")); err != nil {
+		if err := os.MkdirAll(root, 0755); err != nil {
+			return fmt.Errorf("failed to create repository: %v", err)
+		}
+		if err := git("clone", "--depth=1", "https://go.googlesource.com/go", root); err != nil {
+			return fmt.Errorf("failed to clone git repository: %v", err)
+		}
+	}
+
+	// If the argument is a simple decimal number, consider it a CL number.
+	// Otherwise, consider it a branch name. If it's missing, fetch master.
+	if n, _ := strconv.Atoi(target); n >= 1 && strconv.Itoa(n) == target {
+		fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", target)
+		var answer string
+		if fmt.Scanln(&answer); answer != "y" {
+			return fmt.Errorf("interrupted")
+		}
+
+		// ls-remote outputs a number of lines like:
+		// 2621ba2c60d05ec0b9ef37cd71e45047b004cead	refs/changes/37/227037/1
+		// 51f2af2be0878e1541d2769bd9d977a7e99db9ab	refs/changes/37/227037/2
+		// af1f3b008281c61c54a5d203ffb69334b7af007c	refs/changes/37/227037/3
+		// 6a10ebae05ce4b01cb93b73c47bef67c0f5c5f2a	refs/changes/37/227037/meta
+		refs, err := gitOutput("ls-remote")
+		if err != nil {
+			return fmt.Errorf("failed to list remotes: %v", err)
+		}
+		r := regexp.MustCompile(`refs/changes/\d\d/` + target + `/(\d+)`)
+		match := r.FindAllStringSubmatch(string(refs), -1)
+		if match == nil {
+			return fmt.Errorf("CL %v not found", target)
+		}
+		var ref string
+		var patchSet int
+		for _, m := range match {
+			ps, _ := strconv.Atoi(m[1])
+			if ps > patchSet {
+				patchSet = ps
+				ref = m[0]
+			}
+		}
+		log.Printf("Fetching CL %v, Patch Set %v...", target, patchSet)
+		if err := git("fetch", "origin", ref); err != nil {
+			return fmt.Errorf("failed to fetch %s: %v", ref, err)
+		}
+	} else if target != "" {
+		log.Printf("Fetching branch %v...", target)
+		ref := "refs/heads/" + target
+		if err := git("fetch", "origin", ref); err != nil {
+			return fmt.Errorf("failed to fetch %s: %v", ref, err)
+		}
+	} else {
+		log.Printf("Updating the go development tree...")
+		if err := git("fetch", "origin", "master"); err != nil {
+			return fmt.Errorf("failed to fetch git repository updates: %v", err)
+		}
+	}
+
+	// Use checkout and a detached HEAD, because it will refuse to overwrite
+	// local changes, and warn if commits are being left behind, but will not
+	// mind if master is force-pushed upstream.
+	if err := git("-c", "advice.detachedHead=false", "checkout", "FETCH_HEAD"); err != nil {
+		return fmt.Errorf("failed to checkout git repository: %v", err)
+	}
+	// It shouldn't be the case, but in practice sometimes binary artifacts
+	// generated by earlier Go versions interfere with the build.
+	//
+	// Ask the user what to do about them if they are not gitignored. They might
+	// be artifacts that used to be ignored in previous versions, or precious
+	// uncommitted source files.
+	if err := git("clean", "-i", "-d"); err != nil {
+		return fmt.Errorf("failed to cleanup git repository: %v", err)
+	}
+	// Wipe away probably boring ignored files without bothering the user.
+	if err := git("clean", "-q", "-f", "-d", "-X"); err != nil {
+		return fmt.Errorf("failed to cleanup git repository: %v", err)
+	}
+
+	cmd := exec.Command(filepath.Join(root, "src", makeScript()))
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	cmd.Dir = filepath.Join(root, "src")
+	if runtime.GOOS == "windows" {
+		// Workaround make.bat not autodetecting GOROOT_BOOTSTRAP. Issue 28641.
+		goroot, err := exec.Command("go", "env", "GOROOT").Output()
+		if err != nil {
+			return fmt.Errorf("failed to detect an existing go installation for bootstrap: %v", err)
+		}
+		cmd.Env = append(os.Environ(), "GOROOT_BOOTSTRAP="+strings.TrimSpace(string(goroot)))
+	}
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to build go: %v", err)
+	}
+
+	return nil
+}
+
+func makeScript() string {
+	switch runtime.GOOS {
+	case "plan9":
+		return "make.rc"
+	case "windows":
+		return "make.bat"
+	default:
+		return "make.bash"
+	}
+}
+
+var signalsToIgnore = []os.Signal{os.Interrupt, syscall.SIGQUIT}
+
 func init() {
 	http.DefaultTransport = &userAgentTransport{http.DefaultTransport}
 }
@@ -53,20 +214,23 @@
 		log.Fatalf("%s: not downloaded. Run '%s download' to install to %v", version, version, root)
 	}
 
+	runGo(root)
+}
+
+func runGo(root 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
 	}
-	// This envutil.Dedup call is unnecessary when the binary is
-	// built with Go 1.9+, but keep it around for now until Go 1.8
-	// is no longer seen in the wild in common distros.
-	cmd.Env = envutil.Dedup(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))
+	cmd.Env = dedupEnv(caseInsensitiveEnv, append(os.Environ(), "GOROOT="+root, "PATH="+newPath))
+
+	handleSignals()
+
 	if err := cmd.Run(); err != nil {
 		// TODO: return the same exit status maybe.
 		os.Exit(1)
@@ -356,9 +520,17 @@
 	if p.n == p.total {
 		end = ""
 	}
-	fmt.Fprintf(os.Stderr, "Downloaded %0.1f%% (%d / %d bytes)%s\n",
+	fmt.Fprintf(os.Stderr, "Downloaded %5.1f%% (%*d / %d bytes)%s\n",
 		(100.0*float64(p.n))/float64(p.total),
-		p.n, p.total, end)
+		ndigits(p.total), p.n, p.total, end)
+}
+
+func ndigits(i int64) int {
+	var n int
+	for ; i != 0; i /= 10 {
+		n++
+	}
+	return n
 }
 
 func (p *progressWriter) Write(buf []byte) (n int, err error) {
@@ -381,10 +553,6 @@
 func versionArchiveURL(version string) string {
 	goos := getOS()
 
-	// TODO: Maybe we should parse
-	// https://storage.googleapis.com/go-builder-data/dl-index.txt ?
-	// Let's just guess the URL for now and see if it's there.
-	// Then we don't have to maintain that txt file too.
 	ext := ".tar.gz"
 	if goos == "windows" {
 		ext = ".zip"
@@ -393,7 +561,7 @@
 	if goos == "linux" && runtime.GOARCH == "arm" {
 		arch = "armv6l"
 	}
-	return "https://storage.googleapis.com/golang/" + version + "." + goos + "-" + arch + ext
+	return "https://dl.google.com/go/" + version + "." + goos + "-" + arch + ext
 }
 
 const caseInsensitiveEnv = runtime.GOOS == "windows"
@@ -418,6 +586,10 @@
 }
 
 func homedir() (string, error) {
+	// This could be replaced with os.UserHomeDir, but it was introduced too
+	// recently, and we want this to work with go as packaged by Linux
+	// distributions. Note that user.Current is not enough as it does not
+	// prioritize $HOME. See also Issue 26463.
 	switch getOS() {
 	case "plan9":
 		return "", fmt.Errorf("%q not yet supported", runtime.GOOS)
@@ -457,3 +629,43 @@
 	r.Header.Set("User-Agent", "golang-x-build-version/"+version)
 	return uat.rt.RoundTrip(r)
 }
+
+// dedupEnv returns a copy of env with any duplicates removed, in favor of
+// later values.
+// Items are expected to be on the normal environment "key=value" form.
+// If caseInsensitive is true, the case of keys is ignored.
+//
+// This function is unnecessary when the binary is
+// built with Go 1.9+, but keep it around for now until Go 1.8
+// is no longer seen in the wild in common distros.
+//
+// This is copied verbatim from golang.org/x/build/envutil.Dedup at CL 10301
+// (commit a91ae26).
+func dedupEnv(caseInsensitive bool, env []string) []string {
+	out := make([]string, 0, len(env))
+	saw := map[string]int{} // to index in the array
+	for _, kv := range env {
+		eq := strings.Index(kv, "=")
+		if eq < 1 {
+			out = append(out, kv)
+			continue
+		}
+		k := kv[:eq]
+		if caseInsensitive {
+			k = strings.ToLower(k)
+		}
+		if dupIdx, isDup := saw[k]; isDup {
+			out[dupIdx] = kv
+		} else {
+			saw[k] = len(out)
+			out = append(out, kv)
+		}
+	}
+	return out
+}
+
+func handleSignals() {
+	// Ensure that signals intended for the child process are not handled by
+	// this process' runtime (e.g. SIGQUIT). See issue #36976.
+	signal.Notify(make(chan os.Signal), signalsToIgnore...)
+}