blob: d1c69cc79bd44d5ea4e6cf12b9b1985ae215356a [file] [log] [blame]
// 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.
// The gotip command compiles and runs the go command from the development tree.
//
// To install, run:
//
// $ go get golang.org/dl/gotip
// $ gotip download
//
// And then use the gotip command as if it were your normal go command.
//
// To update, run "gotip download" again.
// To download a specific CL, run "gotip download NUMBER".
package main
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
)
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
}