Skip to content

Commit

Permalink
Add support for choosing what binary to run with mage (#146)
Browse files Browse the repository at this point in the history
* add support for choose which go binary to run
* make the compiler configurable
* add a cli flag and update docs
* use go version as part of the binary hash
* add go version to what we display in mage -version
  • Loading branch information
natefinch authored Sep 7, 2018
1 parent e592274 commit bd5486e
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 70 deletions.
4 changes: 3 additions & 1 deletion build-1.11/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (
"strings"
"unicode"
"unicode/utf8"

"github.com/magefile/mage/mg"
)

// A Context specifies the supporting context for a build.
Expand Down Expand Up @@ -1013,7 +1015,7 @@ func (ctxt *Context) importGo(p *Package, path, srcDir string, mode ImportMode,
abs = d
}

cmd := exec.Command("go", "list", "-compiler="+ctxt.Compiler, "-tags="+strings.Join(ctxt.BuildTags, ","), "-installsuffix="+ctxt.InstallSuffix, "-f={{.Dir}}\n{{.ImportPath}}\n{{.Root}}\n{{.Goroot}}\n", path)
cmd := exec.Command(mg.GoCmd(), "list", "-compiler="+ctxt.Compiler, "-tags="+strings.Join(ctxt.BuildTags, ","), "-installsuffix="+ctxt.InstallSuffix, "-f={{.Dir}}\n{{.ImportPath}}\n{{.Root}}\n{{.Goroot}}\n", path)
cmd.Dir = srcDir
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
Expand Down
118 changes: 69 additions & 49 deletions mage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"sort"
"strconv"
"strings"
"text/tabwriter"
"text/template"
"time"
"unicode"
Expand Down Expand Up @@ -55,9 +54,9 @@ var debug = log.New(ioutil.Discard, "DEBUG: ", 0)

// set by ldflags when you "mage build"
var (
commitHash string
timestamp string
gitTag = "unknown"
commitHash = "<not set>"
timestamp = "<not set>"
gitTag = "<not set>"
)

//go:generate stringer -type=Command
Expand Down Expand Up @@ -117,15 +116,10 @@ func ParseAndRun(dir string, stdout, stderr io.Writer, stdin io.Reader, args []s

switch cmd {
case Version:
if timestamp == "" {
timestamp = "<not set>"
}
if commitHash == "" {
commitHash = "<not set>"
}
log.Println("Mage Build Tool", gitTag)
log.Println("Build Date:", timestamp)
log.Println("Commit:", commitHash)
log.Println("built with:", runtime.Version())
return 0
case Init:
if err := generateInit(dir); err != nil {
Expand Down Expand Up @@ -167,39 +161,41 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
var showVersion bool
fs.BoolVar(&showVersion, "version", false, "show version info for the mage binary")

// Categorize commands and options.
commands := []string{"clean", "init", "l", "h", "version"}
options := []string{"f", "keep", "t", "v", "compile", "debug"}

w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)

printUsage := func(flagname string) {
f := fs.Lookup(flagname)
fmt.Fprintf(w, " -%s\t\t%s\n", f.Name, f.Usage)
}

var mageInit bool
fs.BoolVar(&mageInit, "init", false, "create a starting template if no mage files exist")
var clean bool
fs.BoolVar(&clean, "clean", false, "clean out old generated binaries from CACHE_DIR")
var compileOutPath string
fs.StringVar(&compileOutPath, "compile", "", "path to which to output a static binary")
fs.StringVar(&compileOutPath, "compile", "", "output a static binary to the given path")
var goCmd string
fs.StringVar(&goCmd, "gocmd", "", "use the given go binary to compile the output")

fs.Usage = func() {
fmt.Fprintln(w, "mage [options] [target]")
fmt.Fprintln(w, "")
fmt.Fprintln(w, "Commands:")
for _, cmd := range commands {
printUsage(cmd)
}

fmt.Fprintln(w, "")
fmt.Fprintln(w, "Options:")
fmt.Fprintln(w, " -h\t\tshow description of a target")
for _, opt := range options {
printUsage(opt)
}
w.Flush()
fmt.Fprint(stdout, `
mage [options] [target]
Mage is a make-like command runner. See https://magefile.org for full docs.
Commands:
-clean clean out old generated binaries from CACHE_DIR
-compile <string>
output a static binary to the given path
-init create a starting template if no mage files exist
-l list mage targets in this directory
-h show this help
-version show version info for the mage binary
Options:
-debug turn on debug messages (implies -keep)
-h show description of a target
-f force recreation of compiled magefile
-keep keep intermediate mage files around after running
-gocmd <string>
use the given go binary to compile the output (default: "go")
-t <string>
timeout in duration parsable format (e.g. 5m30s)
-v show verbose output when running mage targets
`[1:])
}
err = fs.Parse(args)
if err == flag.ErrHelp {
Expand Down Expand Up @@ -254,6 +250,12 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
inv.Verbose = mg.Verbose()
}

if goCmd != "" {
if err := os.Setenv(mg.GoCmdEnv, goCmd); err != nil {
return inv, cmd, fmt.Errorf("failed to set gocmd: %v", err)
}
}

if numFlags > 1 {
debug.Printf("%d commands defined", numFlags)
return inv, cmd, errors.New("-h, -init, -clean, -compile and -version cannot be used simultaneously")
Expand Down Expand Up @@ -368,25 +370,38 @@ type data struct {
// Yeah, if we get to go2, this will need to be revisited. I think that's ok.
var goVerReg = regexp.MustCompile(`1\.[0-9]+`)

func goVersion() (string, error) {
cmd := exec.Command(mg.GoCmd(), "version")
out, stderr := &bytes.Buffer{}, &bytes.Buffer{}
cmd.Stdout = out
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
if s := stderr.String(); s != "" {
return "", fmt.Errorf("failed to run `go version`: %s", s)
}
return "", fmt.Errorf("failed to run `go version`: %v", err)
}
return out.String(), nil
}

// Magefiles returns the list of magefiles in dir.
func Magefiles(dir string) ([]string, error) {
// use the build directory for the specific go binary we're running. We
// divide the world into two epochs - 1.11 and later, where we have go
// modules, and 1.10 and prior, where there are no modules.
cmd := exec.Command("go", "version")
out, stderr := &bytes.Buffer{}, &bytes.Buffer{}
cmd.Stdout = out
cmd.Stderr = stderr
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed to run `go version`: %s", stderr)
ver, err := goVersion()
if err != nil {
return nil, err
}
v := goVerReg.FindString(out.String())
v := goVerReg.FindString(ver)
if v == "" {
return nil, fmt.Errorf("failed to get version from go version output: %s", out)
log.Println("warning, compiling with unknown go version:", ver)
log.Println("assuming go 1.11+ rules")
v = "1.11"
}
minor, err := strconv.Atoi(v[2:])
if err != nil {
return nil, fmt.Errorf("failed to parse minor version from go version output: %s", out)
return nil, fmt.Errorf("failed to parse minor version from go version: %s", ver)
}
// yes, these two blocks are exactly the same aside from the build context,
// but we need to access struct fields so... let's just copy and paste and
Expand Down Expand Up @@ -430,11 +445,12 @@ func Magefiles(dir string) ([]string, error) {
// Compile uses the go tool to compile the files into an executable at path.
func Compile(path string, stdout, stderr io.Writer, gofiles []string, isdebug bool) error {
debug.Println("compiling to", path)
debug.Println("compiling using gocmd:", mg.GoCmd())
if isdebug {
runDebug("go", "version")
runDebug("go", "env")
runDebug(mg.GoCmd(), "version")
runDebug(mg.GoCmd(), "env")
}
c := exec.Command("go", append([]string{"build", "-o", path}, gofiles...)...)
c := exec.Command(mg.GoCmd(), append([]string{"build", "-o", path}, gofiles...)...)
c.Env = os.Environ()
c.Stderr = stderr
c.Stdout = stdout
Expand Down Expand Up @@ -502,7 +518,11 @@ func ExeName(files []string) (string, error) {
// binary.
hashes = append(hashes, fmt.Sprintf("%x", sha1.Sum([]byte(tpl))))
sort.Strings(hashes)
hash := sha1.Sum([]byte(strings.Join(hashes, "") + magicRebuildKey))
ver, err := goVersion()
if err != nil {
return "", err
}
hash := sha1.Sum([]byte(strings.Join(hashes, "") + magicRebuildKey + ver))
filename := fmt.Sprintf("%x", hash)

out := filepath.Join(mg.CacheDir(), filename)
Expand Down
36 changes: 36 additions & 0 deletions mage/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ import (
"github.com/magefile/mage/mg"
)

const testExeEnv = "MAGE_TEST_STRING"

func TestMain(m *testing.M) {
if s := os.Getenv(testExeEnv); s != "" {
fmt.Fprint(os.Stdout, s)
os.Exit(0)
}
os.Exit(testmain(m))
}

Expand Down Expand Up @@ -620,3 +626,33 @@ func TestClean(t *testing.T) {
t.Errorf("expected '-clean' to remove files from CACHE_DIR, but still have %v", files)
}
}

func TestGoCmd(t *testing.T) {
textOutput := "TestGoCmd"
if err := os.Setenv(testExeEnv, textOutput); err != nil {
t.Fatal(err)
}
if err := os.Setenv(mg.GoCmdEnv, os.Args[0]); err != nil {
t.Fatal(err)
}
defer os.Unsetenv(mg.GoCmdEnv)

// fake out the compiled file, since the code checks for it.
f, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
name := f.Name()
defer os.Remove(name)
f.Close()

buf := &bytes.Buffer{}
stderr := &bytes.Buffer{}
if err := Compile(name, buf, stderr, []string{}, false); err != nil {
t.Log("stderr: ", stderr.String())
t.Fatal(err)
}
if buf.String() != textOutput {
t.Fatal("We didn't run the custom go cmd: ", buf.String())
}
}
16 changes: 7 additions & 9 deletions magefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,19 @@ import (
"strings"
"time"

"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
)

// Runs "go install" for mage. This generates the version info the binary.
func Install() error {
ldf, err := flags()
if err != nil {
return err
}

name := "mage"
if runtime.GOOS == "windows" {
name += ".exe"
}
gopath, err := sh.Output("go", "env", "GOPATH")

gocmd := mg.GoCmd()
gopath, err := sh.Output(gocmd, "env", "GOPATH")
if err != nil {
return fmt.Errorf("can't determine GOPATH: %v", err)
}
Expand All @@ -45,7 +43,7 @@ func Install() error {
// install` turns into a no-op, and `go install -a` fails on people's
// machines that have go installed in a non-writeable directory (such as
// normal OS installs in /usr/bin)
return sh.RunV("go", "build", "-o", path, "-ldflags="+ldf, "github.com/magefile/mage")
return sh.RunV(gocmd, "build", "-o", path, "-ldflags="+flags(), "github.com/magefile/mage")
}

// Generates a new release. Expects the TAG environment variable to be set,
Expand Down Expand Up @@ -74,14 +72,14 @@ func Clean() error {
return sh.Rm("dist")
}

func flags() (string, error) {
func flags() string {
timestamp := time.Now().Format(time.RFC3339)
hash := hash()
tag := tag()
if tag == "" {
tag = "dev"
}
return fmt.Sprintf(`-X "github.com/magefile/mage/mage.timestamp=%s" -X "github.com/magefile/mage/mage.commitHash=%s" -X "github.com/magefile/mage/mage.gitTag=%s"`, timestamp, hash, tag), nil
return fmt.Sprintf(`-X "github.com/magefile/mage/mage.timestamp=%s" -X "github.com/magefile/mage/mage.commitHash=%s" -X "github.com/magefile/mage/mage.gitTag=%s"`, timestamp, hash, tag)
}

// tag returns the git tag for the current branch or "" if none.
Expand Down
14 changes: 14 additions & 0 deletions mg/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ const VerboseEnv = "MAGEFILE_VERBOSE"
// debug mode when running mage.
const DebugEnv = "MAGEFILE_DEBUG"

// GoCmdEnv is the environment variable that indicates the user requested
// verbose mode when running a magefile.
const GoCmdEnv = "MAGEFILE_GOCMD"


// Verbose reports whether a magefile was run with the verbose flag.
func Verbose() bool {
b, _ := strconv.ParseBool(os.Getenv(VerboseEnv))
Expand All @@ -31,6 +36,15 @@ func Debug() bool {
return b
}

// GoCmd reports the command that Mage will use to build go code. By default mage runs
// the "go" binary in the PATH.
func GoCmd() string {
if cmd := os.Getenv(GoCmdEnv); cmd != "" {
return cmd
}
return "go"
}

// CacheDir returns the directory where mage caches compiled binaries. It
// defaults to $HOME/.magefile, but may be overridden by the MAGEFILE_CACHE
// environment variable.
Expand Down
3 changes: 2 additions & 1 deletion parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"os/exec"
"strings"

"github.com/magefile/mage/mg"
mgTypes "github.com/magefile/mage/types"
)

Expand Down Expand Up @@ -317,7 +318,7 @@ func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package,
func makeInfo(dir string, fset *token.FileSet, files map[string]*ast.File) (types.Info, error) {
goroot := os.Getenv("GOROOT")
if goroot == "" {
c := exec.Command("go", "env", "GOROOT")
c := exec.Command(mg.GoCmd(), "env", "GOROOT")
b, err := c.Output()
if err != nil {
return types.Info{}, fmt.Errorf("failed to get GOROOT from 'go env': %v", err)
Expand Down
6 changes: 5 additions & 1 deletion site/content/environment/_index.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ Set to "1" or "true" to turn on debug mode (like running with -debug)

## MAGEFILE_CACHE

Sets the directory where mage will store binaries compiled from magefiles.
Sets the directory where mage will store binaries compiled from magefiles.

## MAGEFILE_GOCMD

Sets the binary that mage will use to compile with (default is "go").
Loading

0 comments on commit bd5486e

Please sign in to comment.