github.com/vektra/tachyon@v0.0.0-20150921164542-0da4f3861aef/builtin.go (about)

     1  package tachyon
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"crypto/md5"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"github.com/flynn/go-shlex"
    10  	"io"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strings"
    15  	"sync"
    16  	"syscall"
    17  )
    18  
    19  func captureCmd(c *exec.Cmd, show bool) ([]byte, []byte, error) {
    20  	stdout, err := c.StdoutPipe()
    21  
    22  	if err != nil {
    23  		return nil, nil, err
    24  	}
    25  
    26  	defer stdout.Close()
    27  
    28  	var wg sync.WaitGroup
    29  
    30  	var bout bytes.Buffer
    31  	var berr bytes.Buffer
    32  
    33  	prefix := []byte(`| `)
    34  
    35  	wg.Add(1)
    36  	go func() {
    37  		defer wg.Done()
    38  		buf := bufio.NewReader(stdout)
    39  
    40  		for {
    41  			line, err := buf.ReadSlice('\n')
    42  
    43  			if err != nil {
    44  				break
    45  			}
    46  
    47  			bout.Write(line)
    48  
    49  			if show {
    50  				os.Stdout.Write(prefix)
    51  				os.Stdout.Write(line)
    52  			}
    53  		}
    54  	}()
    55  
    56  	stderr, err := c.StderrPipe()
    57  
    58  	if err != nil {
    59  		stdout.Close()
    60  		return nil, nil, err
    61  	}
    62  
    63  	defer stderr.Close()
    64  
    65  	wg.Add(1)
    66  	go func() {
    67  		defer wg.Done()
    68  		buf := bufio.NewReader(stderr)
    69  
    70  		for {
    71  			line, err := buf.ReadSlice('\n')
    72  
    73  			if err != nil {
    74  				break
    75  			}
    76  
    77  			berr.Write(line)
    78  
    79  			if show {
    80  				os.Stdout.Write(prefix)
    81  				os.Stdout.Write(line)
    82  			}
    83  		}
    84  	}()
    85  
    86  	c.Start()
    87  
    88  	wg.Wait()
    89  
    90  	err = c.Wait()
    91  
    92  	return bout.Bytes(), berr.Bytes(), err
    93  }
    94  
    95  type CommandResult struct {
    96  	ReturnCode int
    97  	Stdout     []byte
    98  	Stderr     []byte
    99  }
   100  
   101  func RunCommand(env *CommandEnv, parts ...string) (*CommandResult, error) {
   102  	c := exec.Command(parts[0], parts[1:]...)
   103  
   104  	if env.Env.config.ShowCommandOutput {
   105  		fmt.Printf("RUN: %s\n", strings.Join(parts, " "))
   106  	}
   107  
   108  	rc := 0
   109  
   110  	stdout, stderr, err := captureCmd(c, env.Env.config.ShowCommandOutput)
   111  	if err != nil {
   112  		if _, ok := err.(*exec.ExitError); ok {
   113  			rc = 1
   114  		} else {
   115  			return nil, err
   116  		}
   117  	}
   118  
   119  	return &CommandResult{rc, stdout, stderr}, nil
   120  }
   121  
   122  func RunCommandInEnv(env *CommandEnv, unixEnv []string, parts ...string) (*CommandResult, error) {
   123  	c := exec.Command(parts[0], parts[1:]...)
   124  	c.Env = unixEnv
   125  
   126  	if env.Env.config.ShowCommandOutput {
   127  		fmt.Printf("RUN: %s\n", strings.Join(parts, " "))
   128  	}
   129  
   130  	rc := 0
   131  
   132  	stdout, stderr, err := captureCmd(c, env.Env.config.ShowCommandOutput)
   133  	if err != nil {
   134  		if _, ok := err.(*exec.ExitError); ok {
   135  			rc = 1
   136  		} else {
   137  			return nil, err
   138  		}
   139  	}
   140  
   141  	return &CommandResult{rc, stdout, stderr}, nil
   142  }
   143  
   144  func runCmd(env *CommandEnv, ignore bool, parts ...string) (*Result, error) {
   145  	cmd, err := RunCommand(env, parts...)
   146  	if !ignore && err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	r := NewResult(true)
   151  
   152  	r.Add("rc", cmd.ReturnCode)
   153  	r.Add("stdout", strings.TrimSpace(string(cmd.Stdout)))
   154  	r.Add("stderr", strings.TrimSpace(string(cmd.Stderr)))
   155  
   156  	if str, ok := renderShellResult(r); ok {
   157  		r.Add("_result", str)
   158  	}
   159  
   160  	return r, nil
   161  }
   162  
   163  type CommandCmd struct {
   164  	Command    string `tachyon:"command,required"`
   165  	Creates    string `tachyon:"creates"`
   166  	IgnoreFail bool   `tachyon:"ignore_failure"`
   167  }
   168  
   169  func (cmd *CommandCmd) Run(env *CommandEnv) (*Result, error) {
   170  	if cmd.Creates != "" {
   171  		if _, err := os.Stat(cmd.Creates); err == nil {
   172  			r := NewResult(false)
   173  			r.Add("rc", 0)
   174  			r.Add("exists", cmd.Creates)
   175  
   176  			return r, nil
   177  		}
   178  	}
   179  
   180  	parts, err := shlex.Split(cmd.Command)
   181  
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	return runCmd(env, cmd.IgnoreFail, parts...)
   187  }
   188  
   189  func (cmd *CommandCmd) ParseArgs(s Scope, args string) (Vars, error) {
   190  	if args == "" {
   191  		return Vars{}, nil
   192  	}
   193  
   194  	return Vars{"command": Any(args)}, nil
   195  }
   196  
   197  type ShellCmd struct {
   198  	Command    string `tachyon:"command,required"`
   199  	Creates    string `tachyon:"creates"`
   200  	IgnoreFail bool   `tachyon:"ignore_failure"`
   201  }
   202  
   203  func (cmd *ShellCmd) Run(env *CommandEnv) (*Result, error) {
   204  	if cmd.Creates != "" {
   205  		if _, err := os.Stat(cmd.Creates); err == nil {
   206  			r := NewResult(false)
   207  			r.Add("rc", 0)
   208  			r.Add("exists", cmd.Creates)
   209  
   210  			return r, nil
   211  		}
   212  	}
   213  
   214  	return runCmd(env, cmd.IgnoreFail, "sh", "-c", cmd.Command)
   215  }
   216  
   217  func (cmd *ShellCmd) ParseArgs(s Scope, args string) (Vars, error) {
   218  	if args == "" {
   219  		return Vars{}, nil
   220  	}
   221  
   222  	return Vars{"command": Any(args)}, nil
   223  }
   224  
   225  func renderShellResult(res *Result) (string, bool) {
   226  	rcv, ok := res.Get("rc")
   227  	if !ok {
   228  		return "", false
   229  	}
   230  
   231  	stdoutv, ok := res.Get("stdout")
   232  	if !ok {
   233  		return "", false
   234  	}
   235  
   236  	stderrv, ok := res.Get("stderr")
   237  	if !ok {
   238  		return "", false
   239  	}
   240  
   241  	rc := rcv.Read().(int)
   242  	stdout := stdoutv.Read().(string)
   243  	stderr := stderrv.Read().(string)
   244  
   245  	if rc == 0 && len(stdout) == 0 && len(stderr) == 0 {
   246  		return "", true
   247  	} else if len(stderr) == 0 && len(stdout) < 60 {
   248  		stdout = strings.Replace(stdout, "\n", " ", -1)
   249  		return fmt.Sprintf(`rc: %d, stdout: "%s"`, rc, stdout), true
   250  	}
   251  
   252  	return "", false
   253  }
   254  
   255  type CopyCmd struct {
   256  	Src  string `tachyon:"src,required"`
   257  	Dest string `tachyon:"dest,required"`
   258  }
   259  
   260  func md5file(path string) ([]byte, error) {
   261  	h := md5.New()
   262  
   263  	i, err := os.Open(path)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	if _, err := io.Copy(h, i); err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	return h.Sum(nil), nil
   273  }
   274  
   275  func (cmd *CopyCmd) Run(env *CommandEnv) (*Result, error) {
   276  	var src string
   277  
   278  	if cmd.Src[0] == '/' {
   279  		src = cmd.Src
   280  	} else {
   281  		src = env.Paths.File(cmd.Src)
   282  	}
   283  
   284  	input, err := os.Open(src)
   285  
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	srcStat, err := os.Stat(src)
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  
   295  	srcDigest, err := md5file(src)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	var dstDigest []byte
   301  
   302  	defer input.Close()
   303  
   304  	dest := cmd.Dest
   305  
   306  	link := false
   307  
   308  	destStat, err := os.Lstat(dest)
   309  	if err == nil {
   310  		if destStat.IsDir() {
   311  			dest = filepath.Join(dest, filepath.Base(src))
   312  		} else {
   313  			dstDigest, _ = md5file(dest)
   314  		}
   315  
   316  		link = destStat.Mode()&os.ModeSymlink != 0
   317  	}
   318  
   319  	rd := ResultData{
   320  		"md5sum": Any(hex.EncodeToString(srcDigest)),
   321  		"src":    Any(src),
   322  		"dest":   Any(dest),
   323  	}
   324  
   325  	if dstDigest != nil && bytes.Equal(srcDigest, dstDigest) {
   326  		changed := false
   327  
   328  		if destStat.Mode() != srcStat.Mode() {
   329  			changed = true
   330  			if err := os.Chmod(dest, srcStat.Mode()); err != nil {
   331  				return nil, err
   332  			}
   333  		}
   334  
   335  		if ostat, ok := srcStat.Sys().(*syscall.Stat_t); ok {
   336  			if estat, ok := destStat.Sys().(*syscall.Stat_t); ok {
   337  				if ostat.Uid != estat.Uid || ostat.Gid != estat.Gid {
   338  					changed = true
   339  					os.Chown(dest, int(ostat.Uid), int(ostat.Gid))
   340  				}
   341  			}
   342  		}
   343  
   344  		return WrapResult(changed, rd), nil
   345  	}
   346  
   347  	tmp := fmt.Sprintf("%s.tmp.%d", cmd.Dest, os.Getpid())
   348  
   349  	output, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY, 0644)
   350  
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	defer output.Close()
   356  
   357  	if _, err = io.Copy(output, input); err != nil {
   358  		os.Remove(tmp)
   359  		return nil, err
   360  	}
   361  
   362  	if link {
   363  		os.Remove(dest)
   364  	}
   365  
   366  	if err := os.Chmod(tmp, srcStat.Mode()); err != nil {
   367  		os.Remove(tmp)
   368  		return nil, err
   369  	}
   370  
   371  	if ostat, ok := srcStat.Sys().(*syscall.Stat_t); ok {
   372  		os.Chown(tmp, int(ostat.Uid), int(ostat.Gid))
   373  	}
   374  
   375  	err = os.Rename(tmp, dest)
   376  	if err != nil {
   377  		os.Remove(tmp)
   378  		return nil, err
   379  	}
   380  
   381  	return WrapResult(true, rd), nil
   382  }
   383  
   384  type ScriptCmd struct {
   385  	Script     string `tachyon:"command,required"`
   386  	Creates    string `tachyon:"creates"`
   387  	IgnoreFail bool   `tachyon:"ignore_failure"`
   388  }
   389  
   390  func (cmd *ScriptCmd) ParseArgs(s Scope, args string) (Vars, error) {
   391  	if args == "" {
   392  		return Vars{}, nil
   393  	}
   394  
   395  	return Vars{"command": Any(args)}, nil
   396  }
   397  
   398  func (cmd *ScriptCmd) Run(env *CommandEnv) (*Result, error) {
   399  	if cmd.Creates != "" {
   400  		if _, err := os.Stat(cmd.Creates); err == nil {
   401  			r := NewResult(false)
   402  			r.Add("rc", 0)
   403  			r.Add("exists", cmd.Creates)
   404  
   405  			return r, nil
   406  		}
   407  	}
   408  
   409  	script := cmd.Script
   410  
   411  	parts, err := shlex.Split(cmd.Script)
   412  	if err == nil {
   413  		script = parts[0]
   414  	}
   415  
   416  	path := env.Paths.File(script)
   417  
   418  	_, err = os.Stat(path)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  
   423  	runArgs := append([]string{"sh", path}, parts[1:]...)
   424  
   425  	return runCmd(env, cmd.IgnoreFail, runArgs...)
   426  }
   427  
   428  func init() {
   429  	RegisterCommand("command", &CommandCmd{})
   430  	RegisterCommand("shell", &ShellCmd{})
   431  	RegisterCommand("copy", &CopyCmd{})
   432  	RegisterCommand("script", &ScriptCmd{})
   433  }