diff --git a/cmd/getgo/.dockerignore b/cmd/getgo/.dockerignore new file mode 100644 index 00000000..2b87ad9c --- /dev/null +++ b/cmd/getgo/.dockerignore @@ -0,0 +1,5 @@ +.git +.dockerignore +LICENSE +README.md +.gitignore diff --git a/cmd/getgo/.gitignore b/cmd/getgo/.gitignore new file mode 100644 index 00000000..d4984ab9 --- /dev/null +++ b/cmd/getgo/.gitignore @@ -0,0 +1,3 @@ +build +testgetgo +getgo diff --git a/cmd/getgo/Dockerfile b/cmd/getgo/Dockerfile new file mode 100644 index 00000000..78fd9566 --- /dev/null +++ b/cmd/getgo/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:latest + +ENV SHELL /bin/bash +ENV HOME /root +WORKDIR $HOME + +COPY . /go/src/golang.org/x/tools/cmd/getgo + +RUN ( \ + cd /go/src/golang.org/x/tools/cmd/getgo \ + && go build \ + && mv getgo /usr/local/bin/getgo \ + ) + +# undo the adding of GOPATH to env for testing +ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV GOPATH "" + +# delete /go and /usr/local/go for testing +RUN rm -rf /go /usr/local/go diff --git a/cmd/getgo/LICENSE b/cmd/getgo/LICENSE new file mode 100644 index 00000000..32017f8f --- /dev/null +++ b/cmd/getgo/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/getgo/README.md b/cmd/getgo/README.md new file mode 100644 index 00000000..e62a6c2b --- /dev/null +++ b/cmd/getgo/README.md @@ -0,0 +1,71 @@ +# getgo + +A proof-of-concept command-line installer for Go. + +This installer is designed to both install Go as well as do the initial configuration +of setting up the right environment variables and paths. + +It will install the Go distribution (tools & stdlib) to "/.go" inside your home directory by default. + +It will setup "$HOME/go" as your GOPATH. +This is where third party libraries and apps will be installed as well as where you will write your Go code. + +If Go is already installed via this installer it will upgrade it to the latest version of Go. + +Currently the installer supports Windows, \*nix and macOS on x86 & x64. +It supports Bash and Zsh on all of these platforms as well as powershell & cmd.exe on Windows. + +## Usage + +Windows Powershell/cmd.exe: + +`(New-Object System.Net.WebClient).DownloadFile('https://get.golang.org/installer.exe', 'installer.exe'); Start-Process -Wait -NonewWindow installer.exe; Remove-Item installer.exe` + +Shell (Linux/macOS/Windows): + +`curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer` + +## To Do + +* Check if Go is already installed (via a different method) and update it in place or at least notify the user +* Lots of testing. It's only had limited testing so far. +* Add support for additional shells. + +## Development instructions + +### Testing + +There are integration tests in [`main_test.go`](main_test.go). Please add more +tests there. + +#### On unix/linux with the Dockerfile + +The Dockerfile automatically builds the binary, moves it to +`/usr/local/bin/getgo` and then unsets `$GOPATH` and removes all `$GOPATH` from +`$PATH`. + +```bash +$ docker build --rm --force-rm -t getgo . +... +$ docker run --rm -it getgo bash +root@78425260fad0:~# getgo -v +Welcome to the Go installer! +Downloading Go version go1.8.3 to /usr/local/go +This may take a bit of time... +Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin" to /root/.bashrc +Downloaded! +Setting up GOPATH +Adding "export GOPATH=/root/go" to /root/.bashrc +Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin" to /root/.bashrc +GOPATH has been setup! +root@78425260fad0:~# which go +/usr/local/go/bin/go +root@78425260fad0:~# echo $GOPATH +/root/go +root@78425260fad0:~# echo $PATH +/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin +``` + +## Release instructions + +To upload a new release of getgo, run `./make.bash && ./upload.bash`. diff --git a/cmd/getgo/download.go b/cmd/getgo/download.go new file mode 100644 index 00000000..2b9ff6a8 --- /dev/null +++ b/cmd/getgo/download.go @@ -0,0 +1,176 @@ +// Copyright 2017 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 ( + "archive/tar" + "archive/zip" + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" +) + +const ( + currentVersionURL = "https://golang.org/VERSION?m=text" + downloadURLPrefix = "https://storage.googleapis.com/golang" +) + +// downloadGoVersion downloads and upacks the specific go version to dest/go. +func downloadGoVersion(version, ops, arch, dest string) error { + suffix := "tar.gz" + if ops == "windows" { + suffix = "zip" + } + uri := fmt.Sprintf("%s/%s.%s-%s.%s", downloadURLPrefix, version, ops, arch, suffix) + + verbosef("Downloading %s", uri) + + resp, err := http.Get(uri) + if err != nil { + return fmt.Errorf("Downloading Go from %s failed: %v", uri, err) + } + if resp.StatusCode > 299 { + return fmt.Errorf("Downloading Go from %s failed with HTTP status %s", resp.Status) + } + defer resp.Body.Close() + + tmpf, err := ioutil.TempFile("", "go") + if err != nil { + return err + } + defer os.Remove(tmpf.Name()) + + h := sha256.New() + + w := io.MultiWriter(tmpf, h) + if _, err := io.Copy(w, resp.Body); err != nil { + return err + } + + verbosef("Downloading SHA %s.sha256", uri) + + sresp, err := http.Get(uri + ".sha256") + if err != nil { + return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed: %v", uri, err) + } + defer sresp.Body.Close() + if sresp.StatusCode > 299 { + return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", sresp.Status) + } + + shasum, err := ioutil.ReadAll(sresp.Body) + if err != nil { + return err + } + + // Check the shasum. + sum := fmt.Sprintf("%x", h.Sum(nil)) + if sum != string(shasum) { + return fmt.Errorf("Shasum mismatch %s vs. %s", sum, string(shasum)) + } + + unpackFunc := unpackTar + if ops == "windows" { + unpackFunc = unpackZip + } + if err := unpackFunc(tmpf.Name(), dest); err != nil { + return fmt.Errorf("Unpacking Go to %s failed: %v", dest, err) + } + return nil +} + +func unpack(dest, name string, fi os.FileInfo, r io.Reader) error { + if strings.HasPrefix(name, "go/") { + name = name[len("go/"):] + } + + path := filepath.Join(dest, name) + if fi.IsDir() { + return os.MkdirAll(path, fi.Mode()) + } + + f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) + if err != nil { + return err + } + defer f.Close() + + _, err = io.Copy(f, r) + return err +} + +func unpackTar(src, dest string) error { + r, err := os.Open(src) + if err != nil { + return err + } + defer r.Close() + + archive, err := gzip.NewReader(r) + if err != nil { + return err + } + defer archive.Close() + + tarReader := tar.NewReader(archive) + + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if err := unpack(dest, header.Name, header.FileInfo(), tarReader); err != nil { + return err + } + } + + return nil +} + +func unpackZip(src, dest string) error { + zr, err := zip.OpenReader(src) + if err != nil { + return err + } + + for _, f := range zr.File { + fr, err := f.Open() + if err != nil { + return err + } + if err := unpack(dest, f.Name, f.FileInfo(), fr); err != nil { + return err + } + fr.Close() + } + + return nil +} + +func getLatestGoVersion() (string, error) { + resp, err := http.Get(currentVersionURL) + if err != nil { + return "", fmt.Errorf("Getting current Go version failed: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode > 299 { + b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 1024)) + return "", fmt.Errorf("Could not get current Go version: HTTP %d: %q", resp.StatusCode, b) + } + version, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + return strings.TrimSpace(string(version)), nil +} diff --git a/cmd/getgo/download_test.go b/cmd/getgo/download_test.go new file mode 100644 index 00000000..1a47823c --- /dev/null +++ b/cmd/getgo/download_test.go @@ -0,0 +1,34 @@ +// Copyright 2017 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 ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +func TestDownloadGoVersion(t *testing.T) { + if testing.Short() { + t.Skipf("Skipping download in short mode") + } + + tmpd, err := ioutil.TempDir("", "go") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpd) + + if err := downloadGoVersion("go1.8.1", "linux", "amd64", filepath.Join(tmpd, "go")); err != nil { + t.Fatal(err) + } + + // Ensure the VERSION file exists. + vf := filepath.Join(tmpd, "go", "VERSION") + if _, err := os.Stat(vf); os.IsNotExist(err) { + t.Fatalf("file %s does not exist and should", vf) + } +} diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go new file mode 100644 index 00000000..d14f3298 --- /dev/null +++ b/cmd/getgo/main.go @@ -0,0 +1,114 @@ +// Copyright 2017 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 getgo command installs Go to the user's system. +package main + +import ( + "bufio" + "context" + "errors" + "flag" + "fmt" + "os" + "os/exec" + "strings" +) + +var ( + interactive = flag.Bool("i", false, "Prompt for inputs.") + verbose = flag.Bool("v", false, "Verbose.") + setupOnly = flag.Bool("skip-dl", false, "Don't download - only set up environment variables") + goVersion = flag.String("version", "", `Version of Go to install (e.g. "1.8.3"). If empty, uses the latest version.`) + + version = "devel" +) + +var exitCleanly error = errors.New("exit cleanly sentinel value") + +func main() { + flag.Parse() + if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") { + *goVersion = "go" + *goVersion + } + + ctx := context.Background() + + verbosef("version " + version) + + runStep := func(s step) { + err := s(ctx) + if err == exitCleanly { + os.Exit(0) + } + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + } + + if !*setupOnly { + runStep(welcome) + runStep(chooseVersion) + runStep(downloadGo) + } + + runStep(setupGOPATH) +} + +func verbosef(format string, v ...interface{}) { + if !*verbose { + return + } + + fmt.Printf(format+"\n", v...) +} + +func prompt(ctx context.Context, query, defaultAnswer string) (string, error) { + if !*interactive { + return defaultAnswer, nil + } + + fmt.Printf("%s [%s]: ", query, defaultAnswer) + + type result struct { + answer string + err error + } + ch := make(chan result, 1) + go func() { + s := bufio.NewScanner(os.Stdin) + if !s.Scan() { + ch <- result{"", s.Err()} + return + } + answer := s.Text() + if answer == "" { + answer = defaultAnswer + } + ch <- result{answer, nil} + }() + + select { + case r := <-ch: + return r.answer, r.err + case <-ctx.Done(): + return "", ctx.Err() + } +} + +func runCommand(ctx context.Context, prog string, args ...string) ([]byte, error) { + verbosef("Running command: %s %v", prog, args) + + cmd := exec.CommandContext(ctx, prog, args...) + out, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("running cmd '%s %s' failed: %s err: %v", prog, strings.Join(args, " "), string(out), err) + } + if out != nil && err == nil && len(out) != 0 { + verbosef("%s", out) + } + + return out, nil +} diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go new file mode 100644 index 00000000..9da726e4 --- /dev/null +++ b/cmd/getgo/main_test.go @@ -0,0 +1,166 @@ +// Copyright 2017 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 ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "runtime" + "testing" +) + +const ( + testbin = "testgetgo" +) + +var ( + exeSuffix string // ".exe" on Windows +) + +func init() { + if runtime.GOOS == "windows" { + exeSuffix = ".exe" + } +} + +// TestMain creates a getgo command for testing purposes and +// deletes it after the tests have been run. +func TestMain(m *testing.M) { + args := []string{"build", "-tags", testbin, "-o", testbin + exeSuffix} + out, err := exec.Command("go", args...).CombinedOutput() + if err != nil { + fmt.Fprintf(os.Stderr, "building %s failed: %v\n%s", testbin, err, out) + os.Exit(2) + } + + // Don't let these environment variables confuse the test. + os.Unsetenv("GOBIN") + os.Unsetenv("GOPATH") + os.Unsetenv("GIT_ALLOW_PROTOCOL") + os.Unsetenv("PATH") + + r := m.Run() + + os.Remove(testbin + exeSuffix) + + os.Exit(r) +} + +func createTmpHome(t *testing.T) string { + tmpd, err := ioutil.TempDir("", "testgetgo") + if err != nil { + t.Fatalf("creating test tempdir failed: %v", err) + } + + os.Setenv("HOME", tmpd) + return tmpd +} + +// doRun runs the test getgo command, recording stdout and stderr and +// returning exit status. +func doRun(t *testing.T, args ...string) error { + var stdout, stderr bytes.Buffer + t.Logf("running %s %v", testbin, args) + cmd := exec.Command("./"+testbin+exeSuffix, args...) + cmd.Stdout = &stdout + cmd.Stderr = &stderr + cmd.Env = os.Environ() + status := cmd.Run() + if stdout.Len() > 0 { + t.Log("standard output:") + t.Log(stdout.String()) + } + if stderr.Len() > 0 { + t.Log("standard error:") + t.Log(stderr.String()) + } + return status +} + +func TestCommandVerbose(t *testing.T) { + tmpd := createTmpHome(t) + defer os.RemoveAll(tmpd) + + err := doRun(t, "-v") + if err != nil { + t.Fatal(err) + } + // make sure things are in path + shellConfig, err := shellConfigFile() + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadFile(shellConfig) + if err != nil { + t.Fatal(err) + } + home, err := getHomeDir() + if err != nil { + t.Fatal(err) + } + + expected := fmt.Sprintf(` +export PATH=$PATH:%s/.go/bin + +export GOPATH=%s/go + +export PATH=$PATH:%s/go/bin +`, home, home, home) + + if string(b) != expected { + t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) + } +} + +func TestCommandPathExists(t *testing.T) { + tmpd := createTmpHome(t) + defer os.RemoveAll(tmpd) + + // run once + err := doRun(t, "-skip-dl") + if err != nil { + t.Fatal(err) + } + // make sure things are in path + shellConfig, err := shellConfigFile() + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadFile(shellConfig) + if err != nil { + t.Fatal(err) + } + home, err := getHomeDir() + if err != nil { + t.Fatal(err) + } + + expected := fmt.Sprintf(` +export GOPATH=%s/go + +export PATH=$PATH:%s/go/bin +`, home, home) + + if string(b) != expected { + t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) + } + + // run twice + if err := doRun(t, "-skip-dl"); err != nil { + t.Fatal(err) + } + + b, err = ioutil.ReadFile(shellConfig) + if err != nil { + t.Fatal(err) + } + + if string(b) != expected { + t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) + } +} diff --git a/cmd/getgo/make.bash b/cmd/getgo/make.bash new file mode 100755 index 00000000..cbc36857 --- /dev/null +++ b/cmd/getgo/make.bash @@ -0,0 +1,13 @@ +#!/bin/bash + +# Copyright 2017 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. + +set -e -o -x + +LDFLAGS="-X main.version=$(git describe --always --dirty='*')" + +GOOS=windows GOARCH=386 go build -o build/installer.exe -ldflags="$LDFLAGS" +GOOS=linux GOARCH=386 go build -o build/installer_linux -ldflags="$LDFLAGS" +GOOS=darwin GOARCH=386 go build -o build/installer_darwin -ldflags="$LDFLAGS" diff --git a/cmd/getgo/path.go b/cmd/getgo/path.go new file mode 100644 index 00000000..551ac42e --- /dev/null +++ b/cmd/getgo/path.go @@ -0,0 +1,153 @@ +// Copyright 2017 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 ( + "bufio" + "context" + "fmt" + "os" + "os/user" + "path/filepath" + "runtime" + "strings" +) + +const ( + bashConfig = ".bash_profile" + zshConfig = ".zshrc" +) + +// appendToPATH adds the given path to the PATH environment variable and +// persists it for future sessions. +func appendToPATH(value string) error { + if isInPATH(value) { + return nil + } + return persistEnvVar("PATH", pathVar+envSeparator+value) +} + +func isInPATH(dir string) bool { + p := os.Getenv("PATH") + + paths := strings.Split(p, envSeparator) + for _, d := range paths { + if d == dir { + return true + } + } + + return false +} + +func getHomeDir() (string, error) { + home := os.Getenv(homeKey) + if home != "" { + return home, nil + } + + u, err := user.Current() + if err != nil { + return "", err + } + return u.HomeDir, nil +} + +func checkStringExistsFile(filename, value string) (bool, error) { + file, err := os.OpenFile(filename, os.O_RDONLY, 0600) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if line == value { + return true, nil + } + } + + return false, scanner.Err() +} + +func appendToFile(filename, value string) error { + verbosef("Adding %q to %s", value, filename) + + ok, err := checkStringExistsFile(filename, value) + if err != nil { + return err + } + if ok { + // Nothing to do. + return nil + } + + f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(lineEnding + value + lineEnding) + return err +} + +func isShell(name string) bool { + return strings.Contains(currentShell(), name) +} + +// persistEnvVarWindows sets an environment variable in the Windows +// registry. +func persistEnvVarWindows(name, value string) error { + _, err := runCommand(context.Background(), "powershell", "-command", + fmt.Sprintf(`[Environment]::SetEnvironmentVariable("%s", "%s", "User")`, name, value)) + return err +} + +func persistEnvVar(name, value string) error { + if runtime.GOOS == "windows" { + if err := persistEnvVarWindows(name, value); err != nil { + return err + } + + if isShell("cmd.exe") || isShell("powershell.exe") { + return os.Setenv(strings.ToUpper(name), value) + } + // User is in bash, zsh, etc. + // Also set the environment variable in their shell config. + } + + rc, err := shellConfigFile() + if err != nil { + return err + } + + line := fmt.Sprintf("export %s=%s", strings.ToUpper(name), value) + if err := appendToFile(rc, line); err != nil { + return err + } + + return os.Setenv(strings.ToUpper(name), value) +} + +func shellConfigFile() (string, error) { + home, err := getHomeDir() + if err != nil { + return "", err + } + + switch { + case isShell("bash"): + return filepath.Join(home, bashConfig), nil + case isShell("zsh"): + return filepath.Join(home, zshConfig), nil + default: + return "", fmt.Errorf("%q is not a supported shell", currentShell()) + } +} diff --git a/cmd/getgo/path_test.go b/cmd/getgo/path_test.go new file mode 100644 index 00000000..4cf66474 --- /dev/null +++ b/cmd/getgo/path_test.go @@ -0,0 +1,56 @@ +// Copyright 2017 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 ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestAppendPath(t *testing.T) { + tmpd, err := ioutil.TempDir("", "go") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpd) + + if err := os.Setenv("HOME", tmpd); err != nil { + t.Fatal(err) + } + + GOPATH := os.Getenv("GOPATH") + if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { + t.Fatal(err) + } + + shellConfig, err := shellConfigFile() + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadFile(shellConfig) + if err != nil { + t.Fatal(err) + } + + expected := "export PATH=" + pathVar + envSeparator + filepath.Join(GOPATH, "bin") + if strings.TrimSpace(string(b)) != expected { + t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) + } + + // Check that appendToPATH is idempotent. + if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { + t.Fatal(err) + } + b, err = ioutil.ReadFile(shellConfig) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(b)) != expected { + t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) + } +} diff --git a/cmd/getgo/server/README.md b/cmd/getgo/server/README.md new file mode 100644 index 00000000..0cf629d6 --- /dev/null +++ b/cmd/getgo/server/README.md @@ -0,0 +1,7 @@ +# getgo server + +## Deployment + +``` +gcloud app deploy --promote --project golang-org +``` diff --git a/cmd/getgo/server/app.yaml b/cmd/getgo/server/app.yaml new file mode 100644 index 00000000..0502c4e0 --- /dev/null +++ b/cmd/getgo/server/app.yaml @@ -0,0 +1,7 @@ +runtime: go +service: get +api_version: go1 + +handlers: +- url: /.* + script: _go_app diff --git a/cmd/getgo/server/main.go b/cmd/getgo/server/main.go new file mode 100644 index 00000000..0bd33377 --- /dev/null +++ b/cmd/getgo/server/main.go @@ -0,0 +1,64 @@ +// Copyright 2017 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. + +// Command server serves get.golang.org, redirecting users to the appropriate +// getgo installer based on the request path. +package main + +import ( + "fmt" + "net/http" + "strings" + "time" + + "google.golang.org/appengine" +) + +const ( + base = "https://storage.googleapis.com/golang/getgo/" + windowsInstaller = base + "installer.exe" + linuxInstaller = base + "installer_linux" + macInstaller = base + "installer_darwin" +) + +// substring-based redirects. +var stringMatch = map[string]string{ + // via uname, from bash + "MINGW": windowsInstaller, // Reported as MINGW64_NT-10.0 in git bash + "Linux": linuxInstaller, + "Darwin": macInstaller, +} + +func main() { + http.HandleFunc("/", handler) + appengine.Main() +} + +func handler(w http.ResponseWriter, r *http.Request) { + if containsIgnoreCase(r.URL.Path, "installer.exe") { + // cache bust + http.Redirect(w, r, windowsInstaller+cacheBust(), http.StatusFound) + return + } + + for match, redirect := range stringMatch { + if containsIgnoreCase(r.URL.Path, match) { + http.Redirect(w, r, redirect, http.StatusFound) + return + } + } + + http.NotFound(w, r) +} + +func containsIgnoreCase(s, substr string) bool { + return strings.Contains( + strings.ToLower(s), + strings.ToLower(substr), + ) +} + +func cacheBust() string { + return fmt.Sprintf("?%d", time.Now().Nanosecond()) +} diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go new file mode 100644 index 00000000..ad65d456 --- /dev/null +++ b/cmd/getgo/steps.go @@ -0,0 +1,119 @@ +// Copyright 2017 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 ( + "context" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +type step func(context.Context) error + +func welcome(ctx context.Context) error { + fmt.Println("Welcome to the Go installer!") + answer, err := prompt(ctx, "Would you like to install Go? Y/n", "Y") + if err != nil { + return err + } + if strings.ToLower(answer) != "y" { + fmt.Println("Exiting install.") + return exitCleanly + } + + return nil +} + +func chooseVersion(ctx context.Context) error { + // TODO: check if go is currently installed + // TODO: if go is currently installed install new version over that + + if *goVersion != "" { + return nil + } + + var err error + *goVersion, err = getLatestGoVersion() + if err != nil { + return err + } + + answer, err := prompt(ctx, fmt.Sprintf("The latest go version is %s, install that? Y/n", *goVersion), "Y") + if err != nil { + return err + } + + if strings.ToLower(answer) != "y" { + // TODO: handle passing a version + fmt.Println("Aborting install.") + return exitCleanly + } + + return nil +} + +func downloadGo(ctx context.Context) error { + answer, err := prompt(ctx, fmt.Sprintf("Download go version %s to %s? Y/n", *goVersion, installPath), "Y") + if err != nil { + return err + } + + if strings.ToLower(answer) != "y" { + fmt.Println("Aborting install.") + return exitCleanly + } + + fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath) + fmt.Println("This may take a bit of time...") + + if err := downloadGoVersion(*goVersion, runtime.GOOS, arch, installPath); err != nil { + return err + } + + if err := appendToPATH(filepath.Join(installPath, "bin")); err != nil { + return err + } + + fmt.Println("Downloaded!") + return nil +} + +func setupGOPATH(ctx context.Context) error { + answer, err := prompt(ctx, "Would you like us to setup your GOPATH? Y/n", "Y") + if err != nil { + return err + } + + if strings.ToLower(answer) != "y" { + fmt.Println("Exiting and not setting up GOPATH.") + return exitCleanly + } + + fmt.Println("Setting up GOPATH") + home, err := getHomeDir() + if err != nil { + return err + } + + gopath := os.Getenv("GOPATH") + if gopath == "" { + // set $GOPATH + gopath = filepath.Join(home, "go") + if err := persistEnvVar("GOPATH", gopath); err != nil { + return err + } + fmt.Println("GOPATH has been set up!") + } else { + verbosef("GOPATH is already set to %s", gopath) + } + + if err := appendToPATH(filepath.Join(gopath, "bin")); err != nil { + return err + } + return persistEnvChangesForSession() +} diff --git a/cmd/getgo/system.go b/cmd/getgo/system.go new file mode 100644 index 00000000..d424dda9 --- /dev/null +++ b/cmd/getgo/system.go @@ -0,0 +1,29 @@ +// Copyright 2017 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 ( + "bytes" + "os/exec" + "runtime" +) + +// arch contains either amd64 or 386. +var arch = func() string { + cmd := exec.Command("uname", "-m") // "x86_64" + if runtime.GOOS == "windows" { + cmd = exec.Command("powershell", "-command", "(Get-WmiObject -Class Win32_ComputerSystem).SystemType") // "x64-based PC" + } + + out, err := cmd.Output() + if err != nil { + // a sensible default? + return "amd64" + } + if bytes.Contains(out, []byte("64")) { + return "amd64" + } + return "386" +}() diff --git a/cmd/getgo/system_unix.go b/cmd/getgo/system_unix.go new file mode 100644 index 00000000..a3f59578 --- /dev/null +++ b/cmd/getgo/system_unix.go @@ -0,0 +1,50 @@ +// Copyright 2017 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. + +// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris + +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + envSeparator = ":" + homeKey = "HOME" + lineEnding = "\n" + pathVar = "$PATH" +) + +var installPath = func() string { + home, err := getHomeDir() + if err != nil { + return "/usr/local/go" + } + + return filepath.Join(home, ".go") +}() + +func isWindowsXP() bool { + return false +} + +func currentShell() string { + return os.Getenv("SHELL") +} + +func persistEnvChangesForSession() error { + shellConfig, err := shellConfigFile() + if err != nil { + return err + } + fmt.Println() + fmt.Printf("One more thing! Run `source %s` to persist the\n", shellConfig) + fmt.Println("new environment variables to your current session, or open a") + fmt.Println("new shell prompt.") + + return nil +} diff --git a/cmd/getgo/system_windows.go b/cmd/getgo/system_windows.go new file mode 100644 index 00000000..45ce92b9 --- /dev/null +++ b/cmd/getgo/system_windows.go @@ -0,0 +1,81 @@ +// Copyright 2017 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. + +// +build windows + +package main + +import ( + "log" + "os" + "syscall" + "unsafe" +) + +const ( + envSeparator = ";" + homeKey = "USERPROFILE" + lineEnding = "/r/n" + pathVar = "$env:Path" +) + +var installPath = `c:\go` + +func isWindowsXP() bool { + v, err := syscall.GetVersion() + if err != nil { + log.Fatalf("GetVersion failed: %v", err) + } + major := byte(v) + return major < 6 +} + +// currentShell reports the current shell. +// It might be "powershell.exe", "cmd.exe" or any of the *nix shells. +// +// Returns empty string if the shell is unknown. +func currentShell() string { + shell := os.Getenv("SHELL") + if shell != "" { + return shell + } + + pid := os.Getppid() + pe, err := getProcessEntry(pid) + if err != nil { + verbosef("getting shell from process entry failed: %v", err) + return "" + } + + return syscall.UTF16ToString(pe.ExeFile[:]) +} + +func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { + // From https://go.googlesource.com/go/+/go1.8.3/src/syscall/syscall_windows.go#941 + snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) + if err != nil { + return nil, err + } + defer syscall.CloseHandle(snapshot) + + var procEntry syscall.ProcessEntry32 + procEntry.Size = uint32(unsafe.Sizeof(procEntry)) + if err = syscall.Process32First(snapshot, &procEntry); err != nil { + return nil, err + } + + for { + if procEntry.ProcessID == uint32(pid) { + return &procEntry, nil + } + + if err := syscall.Process32Next(snapshot, &procEntry); err != nil { + return nil, err + } + } +} + +func persistEnvChangesForSession() error { + return nil +} diff --git a/cmd/getgo/upload.bash b/cmd/getgo/upload.bash new file mode 100755 index 00000000..f52bb23c --- /dev/null +++ b/cmd/getgo/upload.bash @@ -0,0 +1,19 @@ +#!/bin/bash + +# Copyright 2017 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. + +if ! command -v gsutil 2>&1 > /dev/null; then + echo "Install gsutil:" + echo + echo " https://cloud.google.com/storage/docs/gsutil_install#sdk-install" +fi + +if [ ! -d build ]; then + echo "Run make.bash first" +fi + +set -e -o -x + +gsutil -m cp -a public-read build/* gs://golang/getgo