Skip to content

Commit

Permalink
Compiled flags (magefile#201)
Browse files Browse the repository at this point in the history
* Add mage flags to the compiled mage binary (magefile#180)

Adding following flags:

* -v (verbose)
* -l (list)
* -h (help)
* -t (timeout)

fixes magefile#180
  • Loading branch information
natefinch authored Dec 9, 2018
1 parent f89c4cb commit fe724d1
Show file tree
Hide file tree
Showing 4 changed files with 319 additions and 46 deletions.
29 changes: 17 additions & 12 deletions mage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
// change the inputs to the compiling process.
const magicRebuildKey = "v0.3"

var output = template.Must(template.New("").Funcs(map[string]interface{}{
var mainfileTemplate = template.Must(template.New("").Funcs(map[string]interface{}{
"lower": strings.ToLower,
"lowerFirst": func(s string) string {
parts := strings.Split(s, ":")
Expand All @@ -40,7 +40,7 @@ var output = template.Must(template.New("").Funcs(map[string]interface{}{
}
return strings.Join(parts, ":")
},
}).Parse(tpl))
}).Parse(mageMainfileTplString))
var initOutput = template.Must(template.New("").Parse(mageTpl))

const mainfile = "mage_output_file.go"
Expand Down Expand Up @@ -364,7 +364,12 @@ func Invoke(inv Invocation) int {
}

main := filepath.Join(inv.Dir, mainfile)
err = GenerateMainfile(main, inv.CacheDir, info, imports)
binaryName := "mage"
if inv.CompileOut != "" {
binaryName = filepath.Base(inv.CompileOut)
}

err = GenerateMainfile(binaryName, main, info, imports)
if err != nil {
errlog.Println("Error:", err)
return 1
Expand Down Expand Up @@ -477,12 +482,13 @@ type Import struct {
Info parse.PkgInfo
}

type data struct {
type mainfileTemplateData struct {
Description string
Funcs []*parse.Function
DefaultFunc parse.Function
Aliases map[string]*parse.Function
Imports []*Import
BinaryName string
}

func goVersion(gocmd string) (string, error) {
Expand Down Expand Up @@ -568,7 +574,7 @@ func Compile(magePath, goCmd, compileTo string, gofiles []string, isDebug bool,
for i := range gofiles {
gofiles[i] = filepath.Base(gofiles[i])
}
debug.Printf("running %s -tag=mage build -o %s %s", goCmd, compileTo, strings.Join(gofiles, " "))
debug.Printf("running %s build -o %s %s", goCmd, compileTo, strings.Join(gofiles, " "))
c := exec.Command(goCmd, append([]string{"build", "-o", compileTo}, gofiles...)...)
c.Env = os.Environ()
c.Stderr = stderr
Expand Down Expand Up @@ -614,30 +620,29 @@ func outputDebug(cmd string, args ...string) (string, error) {
return strings.TrimSpace(buf.String()), nil
}

// GenerateMainfile generates the mage mainfile at path. Because go build's
// cache cares about modtimes, we squirrel away our mainfiles in the cachedir
// and move them back as needed.
func GenerateMainfile(path, cachedir string, info *parse.PkgInfo, imports []*Import) error {
// GenerateMainfile generates the mage mainfile at path.
func GenerateMainfile(binaryName, path string, info *parse.PkgInfo, imports []*Import) error {
debug.Println("Creating mainfile at", path)

f, err := os.Create(path)
if err != nil {
return fmt.Errorf("error creating generated mainfile: %v", err)
}
defer f.Close()
data := data{
data := mainfileTemplateData{
Description: info.Description,
Funcs: info.Funcs,
Aliases: info.Aliases,
Imports: imports,
BinaryName: binaryName,
}

if info.DefaultFunc != nil {
data.DefaultFunc = *info.DefaultFunc
}

debug.Println("writing new file at", path)
if err := output.Execute(f, data); err != nil {
if err := mainfileTemplate.Execute(f, data); err != nil {
return fmt.Errorf("can't execute mainfile template: %v", err)
}
if err := f.Close(); err != nil {
Expand Down Expand Up @@ -665,7 +670,7 @@ func ExeName(goCmd, cacheDir string, files []string) (string, error) {
}
// hash the mainfile template to ensure if it gets updated, we make a new
// binary.
hashes = append(hashes, fmt.Sprintf("%x", sha1.Sum([]byte(tpl))))
hashes = append(hashes, fmt.Sprintf("%x", sha1.Sum([]byte(mageMainfileTplString))))
sort.Strings(hashes)
ver, err := goVersion(goCmd)
if err != nil {
Expand Down
183 changes: 178 additions & 5 deletions mage/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func TestIgnoreDefault(t *testing.T) {
Stdout: stdout,
Stderr: stderr,
}
defer os.Setenv(mg.IgnoreDefaultEnv, os.Getenv(mg.IgnoreDefaultEnv))
defer os.Unsetenv(mg.IgnoreDefaultEnv)
if err := os.Setenv(mg.IgnoreDefaultEnv, "1"); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -400,13 +400,13 @@ func TestPanicsErr(t *testing.T) {
// executable name to run, so we automatically create a new exe if the template
// changes.
func TestHashTemplate(t *testing.T) {
templ := tpl
defer func() { tpl = templ }()
templ := mageMainfileTplString
defer func() { mageMainfileTplString = templ }()
name, err := ExeName("go", mg.CacheDir(), []string{"testdata/func.go", "testdata/command.go"})
if err != nil {
t.Fatal(err)
}
tpl = "some other template"
mageMainfileTplString = "some other template"
changed, err := ExeName("go", mg.CacheDir(), []string{"testdata/func.go", "testdata/command.go"})
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -783,6 +783,179 @@ func TestRunCompiledPrintsError(t *testing.T) {
}
}

func TestCompiledFlags(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
dir := "./testdata/compiled"
compileDir, err := ioutil.TempDir(dir, "")
if err != nil {
t.Fatal(err)
}
name := filepath.Join(compileDir, "mage_out")
// The CompileOut directory is relative to the
// invocation directory, so chop off the invocation dir.
outName := "./" + name[len(dir)-1:]
defer os.RemoveAll(compileDir)
inv := Invocation{
Dir: dir,
Stdout: stdout,
Stderr: stderr,
CompileOut: outName,
}
code := Invoke(inv)
if code != 0 {
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
}

run := func(stdout, stderr *bytes.Buffer, filename string, args ...string) error {
stderr.Reset()
stdout.Reset()
cmd := exec.Command(filename, args...)
cmd.Env = os.Environ()
cmd.Stderr = stderr
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("running '%s %s' failed with: %v\nstdout: %s\nstderr: %s",
filename, strings.Join(args, " "), err, stdout, stderr)
}
return nil
}

// get help to target with flag -h target
if err := run(stdout, stderr, name, "-h", "deploy"); err != nil {
t.Fatal(err)
}
got := strings.TrimSpace(stdout.String())
want := filepath.Base(name) + " deploy:\n\nThis is the synopsis for Deploy. This part shouldn't show up."
if got != want {
t.Errorf("got %q, want %q", got, want)
}

// run target with verbose flag -v
if err := run(stdout, stderr, name, "-v", "testverbose"); err != nil {
t.Fatal(err)
}
got = stderr.String()
want = "hi!"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}

// pass list flag -l
if err := run(stdout, stderr, name, "-l"); err != nil {
t.Fatal(err)
}
got = stdout.String()
want = "This is the synopsis for Deploy"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}
want = "This is very verbose"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}

// pass flag -t 1ms
err = run(stdout, stderr, name, "-t", "1ms", "sleep")
if err == nil {
t.Fatalf("expected an error because of timeout")
}
got = stdout.String()
want = "context deadline exceeded"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}
}

func TestCompiledEnvironmentVars(t *testing.T) {
stderr := &bytes.Buffer{}
stdout := &bytes.Buffer{}
dir := "./testdata/compiled"
compileDir, err := ioutil.TempDir(dir, "")
if err != nil {
t.Fatal(err)
}
name := filepath.Join(compileDir, "mage_out")
// The CompileOut directory is relative to the
// invocation directory, so chop off the invocation dir.
outName := "./" + name[len(dir)-1:]
defer os.RemoveAll(compileDir)
inv := Invocation{
Dir: dir,
Stdout: stdout,
Stderr: stderr,
CompileOut: outName,
}
code := Invoke(inv)
if code != 0 {
t.Errorf("expected to exit with code 0, but got %v, stderr: %s", code, stderr)
}

run := func(stdout, stderr *bytes.Buffer, filename string, envval string, args ...string) error {
stderr.Reset()
stdout.Reset()
cmd := exec.Command(filename, args...)
cmd.Env = []string{envval}
cmd.Stderr = stderr
cmd.Stdout = stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("running '%s %s' failed with: %v\nstdout: %s\nstderr: %s",
filename, strings.Join(args, " "), err, stdout, stderr)
}
return nil
}

if err := run(stdout, stderr, name, "MAGEFILE_HELP=1", "deploy"); err != nil {
t.Fatal(err)
}
got := strings.TrimSpace(stdout.String())
want := filepath.Base(name) + " deploy:\n\nThis is the synopsis for Deploy. This part shouldn't show up."
if got != want {
t.Errorf("got %q, want %q", got, want)
}

if err := run(stdout, stderr, name, mg.VerboseEnv+"=1", "testverbose"); err != nil {
t.Fatal(err)
}
got = stderr.String()
want = "hi!"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}

if err := run(stdout, stderr, name, "MAGEFILE_LIST=1"); err != nil {
t.Fatal(err)
}
got = stdout.String()
want = "This is the synopsis for Deploy"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}
want = "This is very verbose"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}

if err := run(stdout, stderr, name, mg.IgnoreDefaultEnv+"=1"); err != nil {
t.Fatal(err)
}
got = stdout.String()
want = "Compiled package description."
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}

err = run(stdout, stderr, name, "MAGEFILE_TIMEOUT=1ms", "sleep")
if err == nil {
t.Fatalf("expected an error because of timeout")
}
got = stdout.String()
want = "context deadline exceeded"
if strings.Contains(got, want) == false {
t.Errorf("got %q, does not contain %q", got, want)
}
}

func TestClean(t *testing.T) {
if err := os.RemoveAll(mg.CacheDir()); err != nil {
t.Error("error removing cache dir:", err)
Expand Down Expand Up @@ -834,10 +1007,10 @@ func TestClean(t *testing.T) {

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

// fake out the compiled file, since the code checks for it.
f, err := ioutil.TempFile("", "")
Expand Down
Loading

0 comments on commit fe724d1

Please sign in to comment.