dev-tools/mage/gotool/go.go (252 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package gotool
import (
"fmt"
"os"
"strings"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)
// Args holds parameters, environment variables and flag information used to
// pass to the go tool.
type Args struct {
extra map[string]string // extra flags one can pass to the command
env map[string]string
flags map[string][]string
pos []string
}
// ArgOpt is a functional option adding info to Args once executed.
type ArgOpt func(args *Args)
type goInstall func(opts ...ArgOpt) error
// Install runs `go install` and provides optionals for adding command line arguments.
var Install goInstall = runGoInstall
func runGoInstall(opts ...ArgOpt) error {
args := buildArgs(opts)
return runVGo("install", args)
}
func (goInstall) Package(pkg string) ArgOpt { return posArg(pkg) }
func (goInstall) Vendored() ArgOpt { return flagArg("-mod", "vendor") }
type goTest func(opts ...ArgOpt) error
// Test runs `go test` and provides optionals for adding command line arguments.
var Test goTest = runGoTest
// GetModuleName returns the name of the module.
func GetModuleName() (string, error) {
lines, err := getLines(callGo(
// Disabling the Go workspaces prevents 'go list' from listing all
// modules within the workspace.
map[string]string{"GOWORK": "off"},
"list",
"-m"))
if err != nil {
return "", err
}
if len(lines) != 1 {
return "", fmt.Errorf("expected 'go list -m' to return 1 line, got %d",
len(lines))
}
return lines[0], nil
}
// ListProjectPackages lists all packages in the current project
func ListProjectPackages() ([]string, error) {
return ListPackages("./...")
}
// ListPackages calls `go list` for every package spec given.
func ListPackages(pkgs ...string) ([]string, error) {
return getLines(callGo(nil, "list", pkgs...))
}
// ListDeps calls `go list -dep` for every package spec given.
func ListDeps(pkg string) ([]string, error) {
const tmpl = `{{if not .Standard}}{{.ImportPath}}{{end}}`
return getLines(callGo(nil, "list", "-deps", "-f", tmpl, pkg))
}
// ListDepsLocation calls `go list -dep` for every package spec given.
func ListDepsLocation(pkg string) (map[string]string, error) {
const tmpl = `{{if not .Standard}}{{.ImportPath}};{{.Dir}}{{end}}`
lines, err := getLines(callGo(nil, "list", "-deps", "-f", tmpl, pkg))
if err != nil {
return nil, err
}
deps := make(map[string]string, len(lines))
for _, l := range lines {
parts := strings.Split(l, ";")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid number of parts")
}
deps[parts[0]] = parts[1]
}
return deps, nil
}
// ListTestFiles lists all go and cgo test files available in a package.
func ListTestFiles(pkg string) ([]string, error) {
const tmpl = `{{ range .TestGoFiles }}{{ printf "%s\n" . }}{{ end }}` +
`{{ range .XTestGoFiles }}{{ printf "%s\n" . }}{{ end }}`
return getLines(callGo(nil, "list", "-f", tmpl, pkg))
}
// ListModuleCacheDir returns the module cache directory containing
// the specified module. If the module does not exist in the cache,
// an error will be returned.
func ListModuleCacheDir(pkg string) (string, error) {
return listModuleDir(pkg, false)
}
// ListModuleVendorDir returns the vendor directory containing the
// specified module. If the module has not been vendored, an error
// will be returned.
func ListModuleVendorDir(pkg string) (string, error) {
return listModuleDir(pkg, true)
}
func listModuleDir(pkg string, vendor bool) (string, error) {
env := map[string]string{
// Make sure GOFLAGS does not influence behaviour.
"GOFLAGS": "",
}
args := []string{"-m", "-f", "{{.Dir}}"}
if vendor {
args = append(args, "-mod=vendor")
}
args = append(args, pkg)
lines, err := getLines(callGo(env, "list", args...))
if err != nil {
return "", err
}
if n := len(lines); n != 1 {
return "", fmt.Errorf("expected 1 line, got %d while looking for %s", n, pkg)
}
return lines[0], nil
}
// HasTests returns true if the given package contains test files.
func HasTests(pkg string) (bool, error) {
files, err := ListTestFiles(pkg)
if err != nil {
return false, err
}
return len(files) > 0, nil
}
func (goTest) WithCoverage(to string) ArgOpt {
return combine(flagArg("-cover", ""), flagArgIf("-test.coverprofile", to))
}
func (goTest) Short(b bool) ArgOpt { return flagBoolIf("-test.short", b) }
func (goTest) Use(bin string) ArgOpt { return extraArgIf("use", bin) }
func (goTest) OS(os string) ArgOpt { return envArgIf("GOOS", os) }
func (goTest) ARCH(arch string) ArgOpt { return envArgIf("GOARCH", arch) }
func (goTest) Create() ArgOpt { return flagArg("-c", "") }
func (goTest) Out(path string) ArgOpt { return flagArg("-o", path) }
func (goTest) Package(path string) ArgOpt { return posArg(path) }
func (goTest) Verbose() ArgOpt { return flagArg("-test.v", "") }
func runGoTest(opts ...ArgOpt) error {
args := buildArgs(opts)
if bin := args.Val("use"); bin != "" {
flags := map[string][]string{}
for k, v := range args.flags {
if strings.HasPrefix(k, "-test.") {
flags[k] = v
}
}
useArgs := &Args{}
*useArgs = *args
useArgs.flags = flags
_, err := sh.Exec(useArgs.env, os.Stdout, os.Stderr, bin, useArgs.build()...)
return err
}
return runVGo("test", args)
}
func getLines(out string, err error) ([]string, error) {
if err != nil {
return nil, err
}
lines := strings.Split(out, "\n")
res := lines[:0]
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) > 0 {
res = append(res, line)
}
}
return res, nil
}
func callGo(env map[string]string, cmd string, opts ...string) (string, error) {
args := []string{cmd}
args = append(args, opts...)
return sh.OutputWith(env, mg.GoCmd(), args...)
}
func runVGo(cmd string, args *Args) error {
return execGoWith(func(env map[string]string, cmd string, args ...string) error {
_, err := sh.Exec(env, os.Stdout, os.Stderr, cmd, args...)
return err
}, cmd, args)
}
func execGoWith(
fn func(map[string]string, string, ...string) error,
cmd string, args *Args,
) error {
cliArgs := []string{cmd}
cliArgs = append(cliArgs, args.build()...)
return fn(args.env, mg.GoCmd(), cliArgs...)
}
func posArg(value string) ArgOpt {
return func(a *Args) { a.Add(value) }
}
func extraArg(k, v string) ArgOpt {
return func(a *Args) { a.Extra(k, v) }
}
func extraArgIf(k, v string) ArgOpt {
if v == "" {
return nil
}
return extraArg(k, v)
}
func envArg(k, v string) ArgOpt {
return func(a *Args) { a.Env(k, v) }
}
func envArgIf(k, v string) ArgOpt {
if v == "" {
return nil
}
return envArg(k, v)
}
func flagArg(flag, value string) ArgOpt {
return func(a *Args) { a.Flag(flag, value) }
}
func flagArgIf(flag, value string) ArgOpt {
if value == "" {
return nil
}
return flagArg(flag, value)
}
func flagBoolIf(flag string, b bool) ArgOpt {
if b {
return flagArg(flag, "")
}
return nil
}
func combine(opts ...ArgOpt) ArgOpt {
return func(a *Args) {
for _, opt := range opts {
if opt != nil {
opt(a)
}
}
}
}
func buildArgs(opts []ArgOpt) *Args {
a := &Args{}
combine(opts...)(a)
return a
}
// Extra sets a special k/v pair to be interpreted by the execution function.
func (a *Args) Extra(k, v string) {
if a.extra == nil {
a.extra = map[string]string{}
}
a.extra[k] = v
}
// Val returns a special functions value for a given key.
func (a *Args) Val(k string) string {
if a.extra == nil {
return ""
}
return a.extra[k]
}
// Env sets an environmant variable to be passed to the child process on exec.
func (a *Args) Env(k, v string) {
if a.env == nil {
a.env = map[string]string{}
}
a.env[k] = v
}
// Flag adds a flag to be passed to the child process on exec.
func (a *Args) Flag(flag, value string) {
if a.flags == nil {
a.flags = map[string][]string{}
}
a.flags[flag] = append(a.flags[flag], value)
}
// Add adds a positional argument to be passed to the child process on exec.
func (a *Args) Add(p string) {
a.pos = append(a.pos, p)
}
func (a *Args) build() []string {
args := make([]string, 0, 2*len(a.flags)+len(a.pos))
for k, values := range a.flags {
for _, v := range values {
args = append(args, k)
if v != "" {
args = append(args, v)
}
}
}
args = append(args, a.pos...)
return args
}