diff --git a/build-1.11/build.go b/build-1.11/build.go index 31411065..622a2bc8 100644 --- a/build-1.11/build.go +++ b/build-1.11/build.go @@ -25,6 +25,8 @@ import ( "strings" "unicode" "unicode/utf8" + + "github.com/magefile/mage/mg" ) // A Context specifies the supporting context for a build. @@ -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 diff --git a/mage/main.go b/mage/main.go index bf0b99c4..f552b54c 100644 --- a/mage/main.go +++ b/mage/main.go @@ -17,7 +17,6 @@ import ( "sort" "strconv" "strings" - "text/tabwriter" "text/template" "time" "unicode" @@ -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 = "" + timestamp = "" + gitTag = "" ) //go:generate stringer -type=Command @@ -117,15 +116,10 @@ func ParseAndRun(dir string, stdout, stderr io.Writer, stdin io.Reader, args []s switch cmd { case Version: - if timestamp == "" { - timestamp = "" - } - if commitHash == "" { - commitHash = "" - } 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 { @@ -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 + 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 + use the given go binary to compile the output (default: "go") + -t + 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 { @@ -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") @@ -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 @@ -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 @@ -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) diff --git a/mage/main_test.go b/mage/main_test.go index 977a015f..2ba73431 100644 --- a/mage/main_test.go +++ b/mage/main_test.go @@ -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)) } @@ -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()) + } +} diff --git a/magefile.go b/magefile.go index 89c73411..1e71dc78 100644 --- a/magefile.go +++ b/magefile.go @@ -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) } @@ -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, @@ -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. diff --git a/mg/runtime.go b/mg/runtime.go index 2936c44e..5c325d2e 100644 --- a/mg/runtime.go +++ b/mg/runtime.go @@ -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)) @@ -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. diff --git a/parse/parse.go b/parse/parse.go index 6607f302..e5d1feaf 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -14,6 +14,7 @@ import ( "os/exec" "strings" + "github.com/magefile/mage/mg" mgTypes "github.com/magefile/mage/types" ) @@ -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) diff --git a/site/content/environment/_index.en.md b/site/content/environment/_index.en.md index 0a35dad9..439b5268 100644 --- a/site/content/environment/_index.en.md +++ b/site/content/environment/_index.en.md @@ -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. \ No newline at end of file +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"). \ No newline at end of file diff --git a/site/content/index.md b/site/content/index.md index efe5f246..202c72a7 100644 --- a/site/content/index.md +++ b/site/content/index.md @@ -53,22 +53,27 @@ plugin. Every tool you use with Go can be used with Magefiles. ``` 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 + -clean clean out old generated binaries from CACHE_DIR + -compile + 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: - -h show description of a target - -f force recreation of compiled magefile - -keep keep intermediate mage files around after running - -t timeout in duration parsable format (e.g. 5m30s) - -v show verbose output when running mage targets - -compile - path to which to output a static binary - -debug turn on debug messages (implies -keep) + -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 + use the given go binary to compile the output (default: "go") + -t + timeout in duration parsable format (e.g. 5m30s) + -v show verbose output when running mage targets ``` ## Why?