From ac1e4b1998e6a311e006d9629b9283295dee36a3 Mon Sep 17 00:00:00 2001 From: Chris Broadfoot Date: Tue, 25 Jul 2017 14:46:40 -0700 Subject: [PATCH 01/13] cmd/getgo: initial commit Initial commit of getgo, a "one-line installer". Example use from bash: curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer It's comprised of two parts: cmd/getgo/server: an App Engine application that redirects users to an appropriate installer based on the request path, which identifies the user's operating system. It's deployed to get.golang.org. cmd/getgo: a cross-compiled binary that does the heavy lifting of downloading and installing the latest Go version (including setting up the environment) to the user's system. The installers are served from the golang GCS bucket. Currently supported systems: linux, darwin, windows / amd64, 386 Authored by Jess Frazelle, Steve Francia, Chris Broadfoot. Change-Id: I615de86e198d3bd93e418fa23055d00ddbdd99fb Reviewed-on: https://go-review.googlesource.com/51115 Reviewed-by: Jaana Burcu Dogan --- cmd/getgo/.dockerignore | 5 + cmd/getgo/.gitignore | 3 + cmd/getgo/Dockerfile | 20 ++++ cmd/getgo/LICENSE | 27 ++++++ cmd/getgo/README.md | 71 +++++++++++++++ cmd/getgo/download.go | 176 ++++++++++++++++++++++++++++++++++++ cmd/getgo/download_test.go | 34 +++++++ cmd/getgo/main.go | 114 +++++++++++++++++++++++ cmd/getgo/main_test.go | 166 ++++++++++++++++++++++++++++++++++ cmd/getgo/make.bash | 13 +++ cmd/getgo/path.go | 153 +++++++++++++++++++++++++++++++ cmd/getgo/path_test.go | 56 ++++++++++++ cmd/getgo/server/README.md | 7 ++ cmd/getgo/server/app.yaml | 7 ++ cmd/getgo/server/main.go | 64 +++++++++++++ cmd/getgo/steps.go | 119 ++++++++++++++++++++++++ cmd/getgo/system.go | 29 ++++++ cmd/getgo/system_unix.go | 50 ++++++++++ cmd/getgo/system_windows.go | 81 +++++++++++++++++ cmd/getgo/upload.bash | 19 ++++ 20 files changed, 1214 insertions(+) create mode 100644 cmd/getgo/.dockerignore create mode 100644 cmd/getgo/.gitignore create mode 100644 cmd/getgo/Dockerfile create mode 100644 cmd/getgo/LICENSE create mode 100644 cmd/getgo/README.md create mode 100644 cmd/getgo/download.go create mode 100644 cmd/getgo/download_test.go create mode 100644 cmd/getgo/main.go create mode 100644 cmd/getgo/main_test.go create mode 100755 cmd/getgo/make.bash create mode 100644 cmd/getgo/path.go create mode 100644 cmd/getgo/path_test.go create mode 100644 cmd/getgo/server/README.md create mode 100644 cmd/getgo/server/app.yaml create mode 100644 cmd/getgo/server/main.go create mode 100644 cmd/getgo/steps.go create mode 100644 cmd/getgo/system.go create mode 100644 cmd/getgo/system_unix.go create mode 100644 cmd/getgo/system_windows.go create mode 100755 cmd/getgo/upload.bash 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 From 5724bdc2fde1ae49ef1d4adfc4971a092333b6dc Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Mon, 31 Jul 2017 12:13:08 -0400 Subject: [PATCH 02/13] x/tools/godoc: fix redirect to Gerrit Redirects to /r/NNNN broke due to crbug.com/gerrit/6888. Alternative URLs are available in the meantime: /NNNN and /c/NNNN. This change uses the /NNNN format. Fixes golang/go#21235 Change-Id: Ie30e01bedd7a8277aedd4070b5f82a754521ed03 Reviewed-on: https://go-review.googlesource.com/52150 Reviewed-by: Kevin Burke --- godoc/redirect/redirect.go | 2 +- godoc/redirect/redirect_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/godoc/redirect/redirect.go b/godoc/redirect/redirect.go index c81d05fc..f86e0ada 100644 --- a/godoc/redirect/redirect.go +++ b/godoc/redirect/redirect.go @@ -196,7 +196,7 @@ func clHandler(w http.ResponseWriter, r *http.Request) { if n, err := strconv.Atoi(id); err == nil && n > 150000 { target = "https://codereview.appspot.com/" + id } else { - target = "https://go-review.googlesource.com/r/" + id + target = "https://go-review.googlesource.com/" + id } http.Redirect(w, r, target, http.StatusFound) } diff --git a/godoc/redirect/redirect_test.go b/godoc/redirect/redirect_test.go index 8a02de32..cfc2f979 100644 --- a/godoc/redirect/redirect_test.go +++ b/godoc/redirect/redirect_test.go @@ -58,8 +58,8 @@ func TestRedirects(t *testing.T) { "/design/123-foo": {302, "https://github.com/golang/proposal/blob/master/design/123-foo.md"}, "/design/text/123-foo": {302, "https://github.com/golang/proposal/blob/master/design/text/123-foo.md"}, - "/cl/1": {302, "https://go-review.googlesource.com/r/1"}, - "/cl/1/": {302, "https://go-review.googlesource.com/r/1"}, + "/cl/1": {302, "https://go-review.googlesource.com/1"}, + "/cl/1/": {302, "https://go-review.googlesource.com/1"}, "/cl/267120043": {302, "https://codereview.appspot.com/267120043"}, "/cl/267120043/": {302, "https://codereview.appspot.com/267120043"}, } From 29518d98f4f8f3676493a890e09e78007b6dcded Mon Sep 17 00:00:00 2001 From: Jaana Burcu Dogan Date: Sat, 29 Jul 2017 12:57:38 -0700 Subject: [PATCH 03/13] cmd/getgo: display that -i stands for interactive mode Change-Id: Ia6d8b0e9b2a355d2c4b41634fd51de51187be833 Reviewed-on: https://go-review.googlesource.com/51931 Reviewed-by: Kevin Burke --- cmd/getgo/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go index d14f3298..58cfbdf1 100644 --- a/cmd/getgo/main.go +++ b/cmd/getgo/main.go @@ -17,7 +17,7 @@ import ( ) var ( - interactive = flag.Bool("i", false, "Prompt for inputs.") + interactive = flag.Bool("i", false, "Interactive mode, 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.`) From 001b4ec8192fbc230137936197bb952603d53f2a Mon Sep 17 00:00:00 2001 From: Jaana Burcu Dogan Date: Sat, 29 Jul 2017 12:25:26 -0700 Subject: [PATCH 04/13] cmd/getgo: have consistent messages Currently, we output: The latest go version is go1.8.3, install that? Y/n [Y]: Y Download go version go1.8.3 to /Users/jbd/.go? Y/n [Y]: Y Change-Id: I4fa72f2066259b75d3349487dae5bdced9fdd8a2 Reviewed-on: https://go-review.googlesource.com/51910 Reviewed-by: Chris Broadfoot --- cmd/getgo/steps.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go index ad65d456..b23f1b77 100644 --- a/cmd/getgo/steps.go +++ b/cmd/getgo/steps.go @@ -43,7 +43,7 @@ func chooseVersion(ctx context.Context) error { return err } - answer, err := prompt(ctx, fmt.Sprintf("The latest go version is %s, install that? Y/n", *goVersion), "Y") + answer, err := prompt(ctx, fmt.Sprintf("The latest Go version is %s, install that? Y/n", *goVersion), "Y") if err != nil { return err } @@ -58,7 +58,7 @@ func chooseVersion(ctx context.Context) error { } func downloadGo(ctx context.Context) error { - answer, err := prompt(ctx, fmt.Sprintf("Download go version %s to %s? Y/n", *goVersion, installPath), "Y") + answer, err := prompt(ctx, fmt.Sprintf("Download Go version %s to %s? Y/n", *goVersion, installPath), "Y") if err != nil { return err } From f2b3bb0049976dc77ab5211f90f5c5e9f1d5b4c1 Mon Sep 17 00:00:00 2001 From: Chris Broadfoot Date: Mon, 31 Jul 2017 11:02:19 -0700 Subject: [PATCH 05/13] cmd/getgo: fix builds A couple fixes: * Disable integration tests in short mode. * Remove import of "google.golang.org/appengine" package. App Engine has two ways to create an app: as a main package and calling appengine.Main(), and as any regular Go package with handlers registered in init(). Change-Id: Ib416111786c1c86cf428d91c60dc406c251d3ca1 Reviewed-on: https://go-review.googlesource.com/52211 Run-TryBot: Chris Broadfoot TryBot-Result: Gobot Gobot Reviewed-by: Jaana Burcu Dogan --- cmd/getgo/main_test.go | 5 +++++ cmd/getgo/server/main.go | 7 ++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go index 9da726e4..932a7397 100644 --- a/cmd/getgo/main_test.go +++ b/cmd/getgo/main_test.go @@ -31,6 +31,11 @@ func init() { // TestMain creates a getgo command for testing purposes and // deletes it after the tests have been run. func TestMain(m *testing.M) { + if os.Getenv("GOGET_INTEGRATION") == "" { + fmt.Fprintln(os.Stderr, "main_test: Skipping integration tests with GOGET_INTEGRATION unset") + return + } + args := []string{"build", "-tags", testbin, "-o", testbin + exeSuffix} out, err := exec.Command("go", args...).CombinedOutput() if err != nil { diff --git a/cmd/getgo/server/main.go b/cmd/getgo/server/main.go index 0bd33377..b5995065 100644 --- a/cmd/getgo/server/main.go +++ b/cmd/getgo/server/main.go @@ -4,15 +4,13 @@ // Command server serves get.golang.org, redirecting users to the appropriate // getgo installer based on the request path. -package main +package server import ( "fmt" "net/http" "strings" "time" - - "google.golang.org/appengine" ) const ( @@ -30,9 +28,8 @@ var stringMatch = map[string]string{ "Darwin": macInstaller, } -func main() { +func init() { http.HandleFunc("/", handler) - appengine.Main() } func handler(w http.ResponseWriter, r *http.Request) { From 6fdd948b8189c73f33b89f4fafb3d1275db66b89 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Mon, 31 Jul 2017 16:04:46 -0400 Subject: [PATCH 06/13] godoc: remove disabled admin code Change-Id: If3e2264b874c7a5447929888fed0ce6ad61f3475 Reviewed-on: https://go-review.googlesource.com/52291 Reviewed-by: Chris Broadfoot --- godoc/dl/dl.go | 7 ------- godoc/dl/tmpl.go | 9 +-------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/godoc/dl/dl.go b/godoc/dl/dl.go index b4230b61..3d2f6c46 100644 --- a/godoc/dl/dl.go +++ b/godoc/dl/dl.go @@ -32,7 +32,6 @@ import ( "google.golang.org/appengine/datastore" "google.golang.org/appengine/log" "google.golang.org/appengine/memcache" - "google.golang.org/appengine/user" ) const ( @@ -183,7 +182,6 @@ var featuredFiles = []Feature{ type listTemplateData struct { Featured []Feature Stable, Unstable, Archive []Release - LoginURL string } var ( @@ -218,11 +216,6 @@ func listHandler(w http.ResponseWriter, r *http.Request) { d.Featured = filesToFeatured(d.Stable[0].Files) } - d.LoginURL, _ = user.LoginURL(c, "/dl") - if user.Current(c) != nil { - d.LoginURL, _ = user.LogoutURL(c, "/dl") - } - item := &memcache.Item{Key: cacheKey, Object: &d, Expiration: cacheDuration} if err := memcache.Gob.Set(c, item); err != nil { log.Errorf(c, "cache set error: %v", err) diff --git a/godoc/dl/tmpl.go b/godoc/dl/tmpl.go index e92f9b76..440917ab 100644 --- a/godoc/dl/tmpl.go +++ b/godoc/dl/tmpl.go @@ -106,7 +106,7 @@ please follow the installation instructions.

-If you are building from source, +If you are building from source, follow the source installation instructions.

@@ -146,13 +146,6 @@ information about Go releases. {{end}} - - - From a237aba5f578100f4f8419bbda73189b8e6dbae5 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Thu, 3 Aug 2017 18:01:02 -0400 Subject: [PATCH 12/13] godoc: fix out-of-bounds panic when serving top-level files Change-Id: I0ba84bac0c97715c0bc66fdc4c33678341ef140c Reviewed-on: https://go-review.googlesource.com/53151 Reviewed-by: Chris Broadfoot Reviewed-by: Brad Fitzpatrick --- godoc/godoc.go | 11 ++++------- godoc/godoc_test.go | 2 ++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/godoc/godoc.go b/godoc/godoc.go index 5c6d9820..d6c27d0b 100644 --- a/godoc/godoc.go +++ b/godoc/godoc.go @@ -461,17 +461,14 @@ func pkgLinkFunc(path string) string { return "pkg/" + path } -// srcToPkgLinkFunc builds an tag linking to -// the package documentation of relpath. +// srcToPkgLinkFunc builds an tag linking to the package +// documentation of relpath. func srcToPkgLinkFunc(relpath string) string { relpath = pkgLinkFunc(relpath) - if relpath == "pkg/" { + relpath = pathpkg.Dir(relpath) + if relpath == "pkg" { return `Index` } - if i := strings.LastIndex(relpath, "/"); i != -1 { - // Remove filename after last slash. - relpath = relpath[:i] - } return fmt.Sprintf(`%s`, relpath, relpath[len("pkg/"):]) } diff --git a/godoc/godoc_test.go b/godoc/godoc_test.go index dca1c951..c1d631c1 100644 --- a/godoc/godoc_test.go +++ b/godoc/godoc_test.go @@ -313,6 +313,8 @@ func TestSrcToPkgLinkFunc(t *testing.T) { }{ {"src/", `Index`}, {"src/fmt/", `fmt`}, + {"pkg/", `Index`}, + {"pkg/LICENSE", `Index`}, } { if got := srcToPkgLinkFunc(tc.path); got != tc.want { t.Errorf("srcToPkgLinkFunc(%v) = %v; want %v", tc.path, got, tc.want) From 0f5d61c4c19cce89dd1354f5d5a97e14bb02d3d2 Mon Sep 17 00:00:00 2001 From: Axel Wagner Date: Sat, 5 Aug 2017 19:29:30 +0200 Subject: [PATCH 13/13] imports: print dir of candidates in addition to import path The import path is ambiguous in the presence of vendoring (e.g. golang/go#20610) Change-Id: I22f372b233b8554e3d9210b383a7df7a6a0f3eee Reviewed-on: https://go-review.googlesource.com/53470 Reviewed-by: Brad Fitzpatrick --- imports/fix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imports/fix.go b/imports/fix.go index ac7f4b00..e2460849 100644 --- a/imports/fix.go +++ b/imports/fix.go @@ -776,7 +776,7 @@ func findImportGoPath(pkgName string, symbols map[string]bool, filename string) sort.Sort(byImportPathShortLength(candidates)) if Debug { for i, pkg := range candidates { - log.Printf("%s candidate %d/%d: %v", pkgName, i+1, len(candidates), pkg.importPathShort) + log.Printf("%s candidate %d/%d: %v in %v", pkgName, i+1, len(candidates), pkg.importPathShort, pkg.dir) } }