Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions internal/dryrun/dryrun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Package dryrun implements the conditional checks for Mage's dryrun mode.
//
// For IsDryRun() to be true, two things have to be true:
// 1. IsPossible() must be true
// - This can only happen if the env var `MAGEFILE_DRYRUN_POSSIBLE` was set at the point of the first call to IsPossible()
//
// 2. IsRequested() must be true
// - This can happen under one of two conditions:
// i. The env var `MAGEFILE_DRYRUN` was set at the point of the first call to IsRequested()
// ii. SetRequested(true) was called at some point prior to the IsPossible() call.
//
// This enables the "top-level" Mage run, which compiles the magefile into a binary, to always be carried out regardless of `-dryrun` (because `MAGEFILE_DRYRUN_POSSIBLE` will not be set in that situation), while still enabling true dryrun functionality for "inner" Mage runs (i.e., runs of the compiled magefile binary).
package dryrun

import (
"os"
"os/exec"
"sync"
)

// RequestedEnv is the environment variable that indicates the user requested dryrun mode when running mage.
const RequestedEnv = "MAGEFILE_DRYRUN"

// PossibleEnv is the environment variable that indicates we are in a context where a dry run is possible.
const PossibleEnv = "MAGEFILE_DRYRUN_POSSIBLE"

var (
// Once-protected variables for whether the user requested dryrun mode.
dryRunRequestedValue bool
dryRunRequestedEnvValue bool
dryRunRequestedEnvOnce sync.Once

// Once-protected variables for whether dryrun mode is possible.
dryRunPossible bool
dryRunPossibleOnce sync.Once
)

// SetRequested sets the dryrun requested state to the specified boolean value.
func SetRequested(value bool) {
dryRunRequestedValue = value
}

// IsRequested checks if dry-run mode was requested, either explicitly or via an environment variable.
func IsRequested() bool {
dryRunRequestedEnvOnce.Do(func() {
if os.Getenv(RequestedEnv) != "" {
dryRunRequestedEnvValue = true
}
})

return dryRunRequestedEnvValue || dryRunRequestedValue
}

// IsPossible checks if dry-run mode is supported in the current context.
func IsPossible() bool {
dryRunPossibleOnce.Do(func() {
dryRunPossible = os.Getenv(PossibleEnv) != ""
})

return dryRunPossible
}

// Wrap creates an *exec.Cmd to run a command or simulate it in dry-run mode.
// If not in dry-run mode, it returns exec.Command(cmd, args...).
// In dry-run mode, it returns a command that prints the simulated command.
func Wrap(cmd string, args ...string) *exec.Cmd {
if !IsDryRun() {
return exec.Command(cmd, args...)
}

// Return an *exec.Cmd that just prints the command that would have been run.
return exec.Command("echo", append([]string{"DRYRUN: " + cmd}, args...)...)
}

// IsDryRun determines if dry-run mode is both possible and requested.
func IsDryRun() bool {
return IsPossible() && IsRequested()
}
72 changes: 72 additions & 0 deletions internal/dryrun/dryrun_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package dryrun

import (
"os"
"os/exec"
"strings"
"testing"
)

// These tests verify dry-run behavior by spawning a fresh process of the
// current test binary with purpose-built helper flags defined in this package's
// TestMain (see testmain_test.go). Spawning a new process ensures the
// sync.Once guards inside dryrun.go evaluate environment variables afresh.

func TestIsDryRunRequestedEnv(t *testing.T) {
cmd := exec.Command(os.Args[0], "-printIsDryRunRequested")
cmd.Env = append(os.Environ(), RequestedEnv+"=1", PossibleEnv+"=1")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("subprocess failed: %v", err)
}
if strings.TrimSpace(string(out)) != "true" {
t.Fatalf("expected true, got %q", strings.TrimSpace(string(out)))
}
}

func TestIsDryRunPossibleEnv(t *testing.T) {
cmd := exec.Command(os.Args[0], "-printIsDryRunPossible")
cmd.Env = append(os.Environ(), PossibleEnv+"=1")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("subprocess failed: %v", err)
}
if strings.TrimSpace(string(out)) != "true" {
t.Fatalf("expected true, got %q", strings.TrimSpace(string(out)))
}
}

func TestIsDryRunRequiresBoth(t *testing.T) {
// Only requested set => not possible, so overall false
cmd := exec.Command(os.Args[0], "-printIsDryRun")
cmd.Env = append(os.Environ(), RequestedEnv+"=1")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("subprocess failed: %v", err)
}
if strings.TrimSpace(string(out)) != "false" {
t.Fatalf("expected false, got %q", strings.TrimSpace(string(out)))
}

// Only possible set => not requested, so overall false
cmd = exec.Command(os.Args[0], "-printIsDryRun")
cmd.Env = append(os.Environ(), PossibleEnv+"=1")
out, err = cmd.CombinedOutput()
if err != nil {
t.Fatalf("subprocess failed: %v", err)
}
if strings.TrimSpace(string(out)) != "false" {
t.Fatalf("expected false, got %q", strings.TrimSpace(string(out)))
}

// Both set => true
cmd = exec.Command(os.Args[0], "-printIsDryRun")
cmd.Env = append(os.Environ(), RequestedEnv+"=1", PossibleEnv+"=1")
out, err = cmd.CombinedOutput()
if err != nil {
t.Fatalf("subprocess failed: %v", err)
}
if strings.TrimSpace(string(out)) != "true" {
t.Fatalf("expected true, got %q", strings.TrimSpace(string(out)))
}
}
37 changes: 37 additions & 0 deletions internal/dryrun/testmain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package dryrun

import (
"flag"
"fmt"
"os"
"testing"
)

var (
printIsDryRunRequested bool
printIsDryRunPossible bool
printIsDryRun bool
)

func init() {
flag.BoolVar(&printIsDryRunRequested, "printIsDryRunRequested", false, "")
flag.BoolVar(&printIsDryRunPossible, "printIsDryRunPossible", false, "")
flag.BoolVar(&printIsDryRun, "printIsDryRun", false, "")
}

func TestMain(m *testing.M) {
flag.Parse()
if printIsDryRunRequested {
fmt.Println(IsRequested())
return
}
if printIsDryRunPossible {
fmt.Println(IsPossible())
return
}
if printIsDryRun {
fmt.Println(IsDryRun())
return
}
os.Exit(m.Run())
}
7 changes: 4 additions & 3 deletions internal/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"runtime"
"strings"

"github.com/magefile/mage/internal/dryrun"
)

var debug *log.Logger = log.New(ioutil.Discard, "", 0)
Expand All @@ -25,7 +26,7 @@ func RunDebug(cmd string, args ...string) error {
buf := &bytes.Buffer{}
errbuf := &bytes.Buffer{}
debug.Println("running", cmd, strings.Join(args, " "))
c := exec.Command(cmd, args...)
c := dryrun.Wrap(cmd, args...)
c.Env = env
c.Stderr = errbuf
c.Stdout = buf
Expand All @@ -45,7 +46,7 @@ func OutputDebug(cmd string, args ...string) (string, error) {
buf := &bytes.Buffer{}
errbuf := &bytes.Buffer{}
debug.Println("running", cmd, strings.Join(args, " "))
c := exec.Command(cmd, args...)
c := dryrun.Wrap(cmd, args...)
c.Env = env
c.Stderr = errbuf
c.Stdout = buf
Expand Down
22 changes: 19 additions & 3 deletions mage/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"io/ioutil"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
Expand All @@ -23,6 +22,7 @@ import (
"time"

"github.com/magefile/mage/internal"
"github.com/magefile/mage/internal/dryrun"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/parse"
"github.com/magefile/mage/sh"
Expand Down Expand Up @@ -107,6 +107,7 @@ type Invocation struct {
List bool // tells the magefile to print out a list of targets
Help bool // tells the magefile to print out help for a specific target
Keep bool // tells mage to keep the generated main file after compiling
DryRun bool // tells mage that all sh.Run* commands should print, but not execute
Timeout time.Duration // tells mage to set a timeout to running the targets
CompileOut string // tells mage to compile a static binary to this path, but not execute
GOOS string // sets the GOOS when producing a binary with -compileout
Expand Down Expand Up @@ -192,6 +193,7 @@ func Parse(stderr, stdout io.Writer, args []string) (inv Invocation, cmd Command
fs.BoolVar(&inv.Help, "h", false, "show this help")
fs.DurationVar(&inv.Timeout, "t", 0, "timeout in duration parsable format (e.g. 5m30s)")
fs.BoolVar(&inv.Keep, "keep", false, "keep intermediate mage files around after running")
fs.BoolVar(&inv.DryRun, "dryrun", false, "print commands instead of executing them")
fs.StringVar(&inv.Dir, "d", "", "directory to read magefiles from")
fs.StringVar(&inv.WorkDir, "w", "", "working directory where magefiles will run")
fs.StringVar(&inv.GoCmd, "gocmd", mg.GoCmd(), "use the given go binary to compile the output")
Expand Down Expand Up @@ -230,6 +232,7 @@ Options:
-d <string>
directory to read magefiles from (default "." or "magefiles" if exists)
-debug turn on debug messages
-dryrun print commands instead of executing them
-f force recreation of compiled magefile
-goarch sets the GOARCH for the binary created by -compile (default: current arch)
-gocmd <string>
Expand Down Expand Up @@ -285,6 +288,10 @@ Options:
debug.SetOutput(stderr)
}

if inv.DryRun {
dryrun.SetRequested(true)
}

inv.CacheDir = mg.CacheDir()

if numCommands > 1 {
Expand Down Expand Up @@ -592,7 +599,7 @@ func Compile(goos, goarch, ldflags, magePath, goCmd, compileTo string, gofiles [
args := append(buildArgs, gofiles...)

debug.Printf("running %s %s", goCmd, strings.Join(args, " "))
c := exec.Command(goCmd, args...)
c := dryrun.Wrap(goCmd, args...)
c.Env = environ
c.Stderr = stderr
c.Stdout = stdout
Expand Down Expand Up @@ -704,17 +711,23 @@ func generateInit(dir string) error {
// RunCompiled runs an already-compiled mage command with the given args,
func RunCompiled(inv Invocation, exePath string, errlog *log.Logger) int {
debug.Println("running binary", exePath)
c := exec.Command(exePath, inv.Args...)
c := dryrun.Wrap(exePath, inv.Args...)
c.Stderr = inv.Stderr
c.Stdout = inv.Stdout
c.Stdin = inv.Stdin
c.Dir = inv.Dir
if inv.WorkDir != inv.Dir {
c.Dir = inv.WorkDir
}

// intentionally pass through unaltered os.Environ here.. your magefile has
// to deal with it.
c.Env = os.Environ()

// We don't want to actually allow dryrun in the outermost invocation of mage, since that will inhibit the very compilation of the magefile & the use of the resulting binary.
// But every situation that's within such an execution is one in which dryrun is supported, so we set this environment variable which will be carried over throughout all such situations.
c.Env = append(c.Env, "MAGEFILE_DRYRUN_POSSIBLE=1")

if inv.Verbose {
c.Env = append(c.Env, "MAGEFILE_VERBOSE=1")
}
Expand All @@ -733,6 +746,9 @@ func RunCompiled(inv Invocation, exePath string, errlog *log.Logger) int {
if inv.Timeout > 0 {
c.Env = append(c.Env, fmt.Sprintf("MAGEFILE_TIMEOUT=%s", inv.Timeout.String()))
}
if inv.DryRun {
c.Env = append(c.Env, "MAGEFILE_DRYRUN=1")
}
debug.Print("running magefile with mage vars:\n", strings.Join(filter(c.Env, "MAGEFILE"), "\n"))
// catch SIGINT to allow magefile to handle them
sigCh := make(chan os.Signal, 1)
Expand Down
8 changes: 8 additions & 0 deletions mg/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"path/filepath"
"runtime"
"strconv"

"github.com/magefile/mage/internal/dryrun"
)

// CacheEnv is the environment variable that users may set to change the
Expand All @@ -19,6 +21,12 @@ const VerboseEnv = "MAGEFILE_VERBOSE"
// debug mode when running mage.
const DebugEnv = "MAGEFILE_DEBUG"

// DryRunRequestedEnv is the environment variable that indicates the user requested dryrun mode when running mage.
const DryRunRequestedEnv = dryrun.RequestedEnv

// DryRunPossibleEnv is the environment variable that indicates we are in a context where a dry run is possible.
const DryRunPossibleEnv = dryrun.PossibleEnv

// GoCmdEnv is the environment variable that indicates the go binary the user
// desires to utilize for Magefile compilation.
const GoCmdEnv = "MAGEFILE_GOCMD"
Expand Down
Loading