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 }