Repository: magefile/mage (go)

Issue:
build mage with mage
we should have a magefile to build all release targets

For now, we don't need a general build target, but it might be worth making one just so people can see how it works.

Pull request:
Add version flag and magefile
This closes #20.  We now can build mage with mage, and embed version
information into the binary.  There's also a target for creating the
binaries for a release.

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d87e092..1c929d6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,14 +22,9 @@ or other trivial change.
 
 ## Dependency Management
 
-Mage uses the official dep tool for managing dependencies.
-
-`go get -u github.com/golang/dep/cmd/dep`
-
-If you add a dependency to the binary, make sure to update the vendor directory
-by running `dep ensure` and adding the resulting files to the repo.
-
-Please try not to add dependencies, though :)
+Currently mage has no dependencies(!).  Let's try to keep it that way.  Since
+it's likely that mage will be vendored into a project, adding dependencies to
+mage adds dependencies to every project that uses mage.
 
 ## Versions
 
diff --git a/README.md b/README.md
index 4a9f5f5..0dfade1 100644
--- a/README.md
+++ b/README.md
@@ -121,6 +121,8 @@ Options:
         keep intermediate mage files around after running
   -l    list mage targets in this directory
   -v    show verbose output when running mage targets
+  -version
+        show version info for the mage binary
 ```
 
 ## Full Example
@@ -229,6 +231,15 @@ will only ever exit with an error code of 0 or 1.  If mage exits with error code
 error code 1.  Why?  Ask the go team.  I've tried to get them to fix it, and
 they won't.
 
+# Helper Libraries
+
+There are two libraries bundled with mage,
+[mg](https://godoc.org/github.com/magefile/mage/mg) and
+[sh](https://godoc.org/github.com/magefile/mage/sh).  Package mg contains
+mage-specific helpers, such as Deps for declaring dependent functions, and
+functions for returning errors with specific error codes that mage understands.
+Package sh contains helpers for running shell-like commands with an API that's
+easier on the eyes and more helpful than os/exec.
 
 # Why?
 
diff --git a/mage/main.go b/mage/main.go
index 1875417..8a0253c 100644
--- a/mage/main.go
+++ b/mage/main.go
@@ -34,8 +34,9 @@ const mainfile = "mage_output_file.go"
 const initFile = "magefile.go"
 
 var (
-	force, verbose, list, help, mageInit, keep bool
-	tags                                       string
+	force, verbose, list, help, mageInit, keep, showVersion bool
+
+	timestamp, commitHash, gitTag string
 )
 
 func init() {
@@ -45,6 +46,7 @@ func init() {
 	flag.BoolVar(&help, "h", false, "show this help")
 	flag.BoolVar(&mageInit, "init", false, "create a starting template if no mage files exist")
 	flag.BoolVar(&keep, "keep", false, "keep intermediate mage files around after running")
+	flag.BoolVar(&showVersion, "version", false, "show version info for the mage binary")
 	flag.Usage = func() {
 		fmt.Println("mage [options] [target]")
 		fmt.Println("Options:")
@@ -62,7 +64,12 @@ func Main() int {
 		flag.Usage()
 		return 0
 	}
-
+	if showVersion {
+		fmt.Println("Mage Build Tool", gitTag)
+		fmt.Println("Build Date:", timestamp)
+		fmt.Println("Commit:", commitHash)
+		return 0
+	}
 	files, err := magefiles()
 	if err != nil {
 		fmt.Println(err)
diff --git a/magefile.go b/magefile.go
new file mode 100644
index 0000000..591716e
--- /dev/null
+++ b/magefile.go
@@ -0,0 +1,73 @@
+//+build mage
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"time"
+
+	"github.com/magefile/mage/sh"
+)
+
+// Runs go install for mage.  This generates the the version
+// info the binary.
+func Build() error {
+	ldf, err := flags()
+	if err != nil {
+		return err
+	}
+
+	return sh.Run("go", "install", "-ldflags="+ldf, "github.com/magefile/mage")
+}
+
+// Generates binaries for all supported platforms.  Currently that means a
+// combination of windows, linux, and OSX in 32 bit and 64 bit formats.  The
+// files will be dumped in the local directory with names according to their
+// supported platform.
+func BuildAll() error {
+	ldf, err := flags()
+	if err != nil {
+		return err
+	}
+	var ext string
+	for _, OS := range []string{"windows", "darwin", "linux"} {
+		if OS == "windows" {
+			ext = ".exe"
+		} else {
+			ext = ""
+		}
+		for _, ARCH := range []string{"amd64", "386"} {
+			log.Printf("running go build for GOOS=%s GOARCH=%s", OS, ARCH)
+			env := map[string]string{"GOOS": OS, "GOARCH": ARCH}
+			if err := sh.RunWith(env, "go", "build", "-o", "mage_"+OS+"_"+ARCH+ext, "--ldflags="+ldf); err != nil {
+				return err
+			}
+		}
+	}
+	return err
+}
+
+func flags() (string, error) {
+	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
+}
+
+// tag returns the git tag for the current branch or "" if none.
+func tag() string {
+	buf := &bytes.Buffer{}
+	_, _ = sh.Exec(nil, buf, nil, "git", "describe", "--tags")
+	return buf.String()
+}
+
+// hash returns the git hash for the current repo or "" if none.
+func hash() string {
+	hash, _ := sh.Output("git", "rev-parse", "--short", "HEAD")
+	return hash
+}
diff --git a/sh/cmd.go b/sh/cmd.go
index 9ac1b4e..9c5c880 100644
--- a/sh/cmd.go
+++ b/sh/cmd.go
@@ -59,21 +59,21 @@ func RunWith(env map[string]string, cmd string, args ...string) error {
 	if mg.Verbose() {
 		output = os.Stdout
 	}
-	_, err := Exec(env, output, cmd, args...)
+	_, err := Exec(env, output, os.Stderr, cmd, args...)
 	return err
 }
 
 // Output runs the command and returns the text from stdout.
 func Output(cmd string, args ...string) (string, error) {
 	buf := &bytes.Buffer{}
-	_, err := Exec(nil, buf, cmd, args...)
+	_, err := Exec(nil, buf, os.Stderr, cmd, args...)
 	return strings.TrimSuffix(buf.String(), "\n"), err
 }
 
 // OutputWith is like RunWith, ubt returns what is written to stdout.
 func OutputWith(env map[string]string, cmd string, args ...string) (string, error) {
 	buf := &bytes.Buffer{}
-	_, err := Exec(env, buf, cmd, args...)
+	_, err := Exec(env, buf, os.Stderr, cmd, args...)
 	return strings.TrimSuffix(buf.String(), "\n"), err
 }
 
@@ -89,7 +89,7 @@ func OutputWith(env map[string]string, cmd string, args ...string) (string, erro
 // Ran reports if the command ran (rather than was not found or not executable).
 // Code reports the exit code the command returned if it ran. If err == nil, ran
 // is always true and code is always 0.
-func Exec(env map[string]string, stdout io.Writer, cmd string, args ...string) (ran bool, err error) {
+func Exec(env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, err error) {
 	expand := func(s string) string {
 		s2, ok := env[s]
 		if ok {
@@ -101,7 +101,7 @@ func Exec(env map[string]string, stdout io.Writer, cmd string, args ...string) (
 	for i := range args {
 		args[i] = os.Expand(args[i], expand)
 	}
-	ran, code, err := run(env, stdout, cmd, args...)
+	ran, code, err := run(env, stdout, stderr, cmd, args...)
 	if err == nil {
 		return true, nil
 	}
@@ -111,13 +111,13 @@ func Exec(env map[string]string, stdout io.Writer, cmd string, args ...string) (
 	return ran, fmt.Errorf(`failed to run "%s %s: %v"`, cmd, strings.Join(args, " "), err)
 }
 
-func run(env map[string]string, stdout io.Writer, cmd string, args ...string) (ran bool, code int, err error) {
+func run(env map[string]string, stdout, stderr io.Writer, cmd string, args ...string) (ran bool, code int, err error) {
 	c := exec.Command(cmd, args...)
 	c.Env = os.Environ()
 	for k, v := range env {
 		c.Env = append(c.Env, k+"="+v)
 	}
-	c.Stderr = os.Stderr
+	c.Stderr = stderr
 	c.Stdout = stdout
 	log.Println("exec:", cmd, strings.Join(args, " "))
 	err = c.Run()


Test patch:
diff --git a/sh/cmd_test.go b/sh/cmd_test.go
index 0edc002..6ed469e 100644
--- a/sh/cmd_test.go
+++ b/sh/cmd_test.go
@@ -19,7 +19,7 @@ func TestOutCmd(t *testing.T) {
 }
 
 func TestExitCode(t *testing.T) {
-	ran, err := Exec(nil, nil, os.Args[0], "-helper", "-exit", "99")
+	ran, err := Exec(nil, nil, nil, os.Args[0], "-helper", "-exit", "99")
 	if err == nil {
 		t.Fatal("unexpected nil error from run")
 	}
@@ -35,7 +35,7 @@ func TestExitCode(t *testing.T) {
 func TestEnv(t *testing.T) {
 	env := "SOME_REALLY_LONG_MAGEFILE_SPECIFIC_THING"
 	out := &bytes.Buffer{}
-	ran, err := Exec(map[string]string{env: "foobar"}, out, os.Args[0], "-printVar", env)
+	ran, err := Exec(map[string]string{env: "foobar"}, out, nil, os.Args[0], "-printVar", env)
 	if err != nil {
 		t.Fatalf("unexpected error from runner: %#v", err)
 	}
@@ -48,7 +48,7 @@ func TestEnv(t *testing.T) {
 }
 
 func TestNotRun(t *testing.T) {
-	ran, err := Exec(nil, nil, "thiswontwork")
+	ran, err := Exec(nil, nil, nil, "thiswontwork")
 	if err == nil {
 		t.Fatal("unexpected nil error")
 	}