tools/installtools/main.go (154 lines of code) (raw):

// Copyright 2021 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. // Binary installtools is a helper that installs Go tools extension tests depend on. package main import ( "bytes" "fmt" "os" "os/exec" "path" "path/filepath" "runtime" "strings" ) // finalVersion encodes the fact that the specified tool version // is the known last version that can be buildable with goMinorVersion. type finalVersion struct { goMinorVersion int version string } var tools = []struct { path string dest string preferPreview bool // versions is a list of supportedVersions sorted by // goMinorVersion. If we want to pin a tool's version // add a fake entry with a large goMinorVersion // value and the pinned tool version as the last entry. // Nil of empty list indicates we can use the `latest` version. versions []finalVersion }{ // TODO: auto-generate based on allTools.ts.in. {"golang.org/x/tools/gopls", "", true, nil}, {"github.com/acroca/go-symbols", "", false, nil}, {"github.com/cweill/gotests/gotests", "", false, nil}, {"github.com/davidrjenni/reftools/cmd/fillstruct", "", false, nil}, {"github.com/haya14busa/goplay/cmd/goplay", "", false, nil}, {"github.com/stamblerre/gocode", "gocode-gomod", false, nil}, {"github.com/mdempsky/gocode", "", false, nil}, {"github.com/ramya-rao-a/go-outline", "", false, nil}, {"github.com/rogpeppe/godef", "", false, nil}, {"github.com/sqs/goreturns", "", false, nil}, {"github.com/uudashr/gopkgs/v2/cmd/gopkgs", "", false, nil}, {"github.com/zmb3/gogetdoc", "", false, nil}, {"honnef.co/go/tools/cmd/staticcheck", "", false, []finalVersion{{16, "v0.2.2"}, {18, "v0.3.3"}}}, {"golang.org/x/tools/cmd/gorename", "", false, nil}, {"github.com/go-delve/delve/cmd/dlv", "", false, []finalVersion{{16, "v1.8.3"}, {17, "v1.9.1"}}}, } // pickVersion returns the version to install based on the supported // version list. func pickVersion(goMinorVersion int, versions []finalVersion, defaultVersion string) string { for _, v := range versions { if goMinorVersion <= v.goMinorVersion { return v.version } } return defaultVersion } func main() { ver, err := goVersion() if err != nil { exitf("failed to find go version: %v", err) } if ver < 1 { exitf("unsupported go version: 1.%v", ver) } bin, err := goBin() if err != nil { exitf("failed to determine go tool installation directory: %v", err) } err = installTools(bin, ver) if err != nil { exitf("failed to install tools: %v", err) } } func exitf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, format, args...) os.Exit(1) } // goVersion returns an integer N if go's version is 1.N. func goVersion() (int, error) { cmd := exec.Command("go", "list", "-e", "-f", `{{context.ReleaseTags}}`, "--", "unsafe") cmd.Env = append(os.Environ(), "GO111MODULE=off") out, err := cmd.Output() if err != nil { return 0, fmt.Errorf("go list error: %v", err) } result := string(out) if len(result) < 3 { return 0, fmt.Errorf("bad ReleaseTagsOutput: %q", result) } // Split up "[go1.1 go1.15]" tags := strings.Fields(result[1 : len(result)-2]) for i := len(tags) - 1; i >= 0; i-- { var version int if _, err := fmt.Sscanf(tags[i], "go1.%d", &version); err != nil { continue } return version, nil } return 0, fmt.Errorf("no parseable ReleaseTags in %v", tags) } // goBin returns the directory where the go command will install binaries. func goBin() (string, error) { if gobin := os.Getenv("GOBIN"); gobin != "" { return gobin, nil } out, err := exec.Command("go", "env", "GOPATH").Output() if err != nil { return "", err } gopaths := filepath.SplitList(strings.TrimSpace(string(out))) if len(gopaths) == 0 { return "", fmt.Errorf("invalid GOPATH: %s", out) } return filepath.Join(gopaths[0], "bin"), nil } func installTools(binDir string, goMinorVersion int) error { installCmd := "install" if goMinorVersion < 16 { installCmd = "get" } dir := "" if installCmd == "get" { // run `go get` command from an empty directory. dir = os.TempDir() } env := append(os.Environ(), "GO111MODULE=on") for _, tool := range tools { ver := pickVersion(goMinorVersion, tool.versions, pickLatest(tool.path, tool.preferPreview)) path := tool.path + "@" + ver cmd := exec.Command("go", installCmd, path) cmd.Env = env cmd.Dir = dir fmt.Println("go", installCmd, path) if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("installing %v: %s\n%v", path, out, err) } loc := filepath.Join(binDir, binName(tool.path)) if tool.dest != "" { newLoc := filepath.Join(binDir, binName(tool.dest)) if err := os.Rename(loc, newLoc); err != nil { return fmt.Errorf("copying %v to %v: %v", loc, newLoc, err) } loc = newLoc } fmt.Println("\tinstalled", loc) } return nil } func binName(toolPath string) string { b := path.Base(toolPath) if runtime.GOOS == "windows" { return b + ".exe" } return b } func pickLatest(toolPath string, preferPreview bool) string { if !preferPreview { return "latest" // should we pick the pinned version in allTools.ts.in? } out, err := exec.Command("go", "list", "-m", "--versions", toolPath).Output() if err != nil { exitf("failed to find a suitable version for %q: %v", toolPath, err) } versions := bytes.Split(out, []byte(" ")) if len(versions) == 0 { exitf("failed to find a suitable version for %q: %s", toolPath, out) } return string(bytes.TrimSpace(versions[len(versions)-1])) }