gotip: fold into internal/version
Essentially a rebase of CL 217764 by zikaeroh, but with separate files
so that it's easier to review.
Fixes golang/go#37037
Change-Id: I8e74671544c3af1883d6958f2357ba1923175950
Reviewed-on: https://go-review.googlesource.com/c/dl/+/301909
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Go Bot <gobot@golang.org>
Trust: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/gotip/main.go b/gotip/main.go
index d1c69cc..7e636ac 100644
--- a/gotip/main.go
+++ b/gotip/main.go
@@ -16,257 +16,9 @@
package main
import (
- "errors"
- "fmt"
- "log"
- "os"
- "os/exec"
- "os/user"
- "path/filepath"
- "regexp"
- "runtime"
- "strconv"
- "strings"
+ "golang.org/dl/internal/version"
)
func main() {
- 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 := strconv.Atoi(os.Args[2]); err != nil {
- log.Fatalf("gotip: invalid CL number: %q", os.Args[2])
- }
- if err := installTip(root, os.Args[2]); err != nil {
- log.Fatalf("gotip: %v", err)
- }
- default:
- log.Fatalf("gotip: usage: gotip download [CL number]")
- }
- 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)
- }
-
- 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))
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- // TODO: return the same exit status maybe.
- os.Exit(1)
- }
- log.Fatalf("gotip: failed to execute %v: %v", gobin, err)
- }
- os.Exit(0)
-}
-
-func installTip(root, clNumber 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 clNumber != "" {
- fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", clNumber)
- 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/` + clNumber + `/(\d+)`)
- match := r.FindAllStringSubmatch(string(refs), -1)
- if match == nil {
- return fmt.Errorf("CL %v not found", clNumber)
- }
- 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...", clNumber, patchSet)
- 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"
- }
-}
-
-const caseInsensitiveEnv = runtime.GOOS == "windows"
-
-func exe() string {
- if runtime.GOOS == "windows" {
- return ".exe"
- }
- return ""
-}
-
-func goroot(version string) (string, error) {
- home, err := homedir()
- if err != nil {
- return "", fmt.Errorf("failed to get home directory: %v", err)
- }
- return filepath.Join(home, "sdk", version), nil
-}
-
-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 runtime.GOOS {
- case "plan9":
- return "", fmt.Errorf("%q not yet supported", runtime.GOOS)
- case "windows":
- if dir := os.Getenv("USERPROFILE"); dir != "" {
- return dir, nil
- }
- return "", errors.New("can't find user home directory; %USERPROFILE% is empty")
- default:
- if dir := os.Getenv("HOME"); dir != "" {
- return dir, nil
- }
- if u, err := user.Current(); err == nil && u.HomeDir != "" {
- return u.HomeDir, nil
- }
- return "", errors.New("can't find user home directory; $HOME is empty")
- }
-}
-
-// 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
+ version.RunTip()
}
diff --git a/gotip/main_test.go b/gotip/main_test.go
deleted file mode 100644
index d15f103..0000000
--- a/gotip/main_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright 2019 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 main
-
-import (
- "reflect"
- "testing"
-)
-
-func TestDedupEnv(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 := dedupEnv(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/version/gotip.go b/internal/version/gotip.go
new file mode 100644
index 0000000..14ce7ed
--- /dev/null
+++ b/internal/version/gotip.go
@@ -0,0 +1,169 @@
+// Copyright 2018 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 version
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+)
+
+// 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 := strconv.Atoi(os.Args[2]); err != nil {
+ log.Fatalf("gotip: invalid CL number: %q", os.Args[2])
+ }
+ if err := installTip(root, os.Args[2]); err != nil {
+ log.Fatalf("gotip: %v", err)
+ }
+ default:
+ log.Fatalf("gotip: usage: gotip download [CL number]")
+ }
+ 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, clNumber 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 clNumber != "" {
+ fmt.Fprintf(os.Stderr, "This will download and execute code from golang.org/cl/%s, continue? [y/n] ", clNumber)
+ 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/` + clNumber + `/(\d+)`)
+ match := r.FindAllStringSubmatch(string(refs), -1)
+ if match == nil {
+ return fmt.Errorf("CL %v not found", clNumber)
+ }
+ 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...", clNumber, patchSet)
+ 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"
+ }
+}
diff --git a/internal/version/version.go b/internal/version/version.go
index a63c649..866e4ad 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -50,6 +50,10 @@
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
@@ -419,6 +423,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)