github.phpd.cn/thought-machine/please@v12.2.0+incompatible/src/run/run_step.go (about) 1 // Package run implements the "plz run" command. 2 package run 3 4 import ( 5 "fmt" 6 "os" 7 "os/exec" 8 "strings" 9 "sync" 10 "syscall" 11 12 "golang.org/x/sync/errgroup" 13 "gopkg.in/op/go-logging.v1" 14 15 "build" 16 "core" 17 "output" 18 ) 19 20 var log = logging.MustGetLogger("run") 21 22 // Run implements the running part of 'plz run'. 23 func Run(state *core.BuildState, label core.BuildLabel, args []string, env bool) { 24 run(state, label, args, false, false, env) 25 } 26 27 // Parallel runs a series of targets in parallel. 28 // Returns true if all were successful. 29 func Parallel(state *core.BuildState, labels []core.BuildLabel, args []string, numTasks int, quiet, env bool) int { 30 pool := NewGoroutinePool(numTasks) 31 var g errgroup.Group 32 for _, label := range labels { 33 label := label // capture locally 34 g.Go(func() (err error) { 35 var wg sync.WaitGroup 36 wg.Add(1) 37 pool.Submit(func() { 38 if e := run(state, label, args, true, quiet, env); e != nil { 39 err = e 40 } 41 wg.Done() 42 }) 43 wg.Wait() 44 return 45 }) 46 } 47 if err := g.Wait(); err != nil { 48 log.Error("Command failed: %s", err) 49 return err.(*exitError).code 50 } 51 return 0 52 } 53 54 // Sequential runs a series of targets sequentially. 55 // Returns true if all were successful. 56 func Sequential(state *core.BuildState, labels []core.BuildLabel, args []string, quiet, env bool) int { 57 for _, label := range labels { 58 log.Notice("Running %s", label) 59 if err := run(state, label, args, true, quiet, env); err != nil { 60 log.Error("%s", err) 61 return err.code 62 } 63 } 64 return 0 65 } 66 67 // run implements the internal logic about running a target. 68 // If fork is true then we fork to run the target and return any error from the subprocesses. 69 // If it's false this function never returns (because we either win or die; it's like 70 // Game of Thrones except rather less glamorous). 71 func run(state *core.BuildState, label core.BuildLabel, args []string, fork, quiet, setenv bool) *exitError { 72 target := state.Graph.TargetOrDie(label) 73 if !target.IsBinary { 74 log.Fatalf("Target %s cannot be run; it's not marked as binary", label) 75 } 76 // ReplaceSequences always quotes stuff in case it contains spaces or special characters, 77 // that works fine if we interpret it as a shell but not to pass it as an argument here. 78 arg0 := strings.Trim(build.ReplaceSequences(state, target, fmt.Sprintf("$(out_exe %s)", target.Label)), "\"") 79 // Handle targets where $(exe ...) returns something nontrivial 80 splitCmd := strings.Split(arg0, " ") 81 if !strings.Contains(splitCmd[0], "/") { 82 // Probably it's a java -jar, we need an absolute path to it. 83 cmd, err := exec.LookPath(splitCmd[0]) 84 if err != nil { 85 log.Fatalf("Can't find binary %s", splitCmd[0]) 86 } 87 splitCmd[0] = cmd 88 } 89 args = append(splitCmd, args...) 90 log.Info("Running target %s...", strings.Join(args, " ")) 91 output.SetWindowTitle("plz run: " + strings.Join(args, " ")) 92 env := environ(state.Config, setenv) 93 if !fork { 94 // Plain 'plz run'. One way or another we never return from the following line. 95 must(syscall.Exec(splitCmd[0], args, env), args) 96 } 97 // Run as a normal subcommand. 98 // Note that we don't connect stdin. It doesn't make sense for multiple processes. 99 cmd := core.ExecCommand(splitCmd[0], args[1:]...) // args here don't include argv[0] 100 cmd.Env = env 101 if !quiet { 102 cmd.Stdout = os.Stdout 103 cmd.Stderr = os.Stderr 104 must(cmd.Start(), args) 105 err := cmd.Wait() 106 return toExitError(err, cmd, nil) 107 } 108 out, err := cmd.CombinedOutput() 109 return toExitError(err, cmd, out) 110 } 111 112 // environ returns an appropriate environment for a command. 113 func environ(config *core.Configuration, setenv bool) []string { 114 env := os.Environ() 115 if setenv { 116 for _, e := range core.GeneralBuildEnvironment(config) { 117 env = addEnv(env, e) 118 } 119 } 120 return env 121 } 122 123 // addEnv adds an env var to an existing set, with replacement. 124 func addEnv(env []string, e string) []string { 125 name := e[:strings.IndexRune(e, '=')+1] 126 for i, existing := range env { 127 if strings.HasPrefix(existing, name) { 128 env[i] = e 129 return env 130 } 131 } 132 return append(env, e) 133 } 134 135 // must dies if the given error is non-nil. 136 func must(err error, cmd []string) { 137 if err != nil { 138 log.Fatalf("Error running command %s: %s", strings.Join(cmd, " "), err) 139 } 140 } 141 142 // toExitError attempts to extract the exit code from an error. 143 func toExitError(err error, cmd *exec.Cmd, out []byte) *exitError { 144 exitCode := 1 145 if err == nil { 146 return nil 147 } else if exitError, ok := err.(*exec.ExitError); ok { 148 // This is a little hairy; there isn't a good way of getting the exit code, 149 // but this should be reasonably portable (at least to the platforms we care about). 150 if status, ok := exitError.Sys().(syscall.WaitStatus); ok { 151 exitCode = status.ExitStatus() 152 } 153 } 154 return &exitError{ 155 msg: fmt.Sprintf("Error running command %s: %s\n%s", strings.Join(cmd.Args, " "), err, string(out)), 156 code: exitCode, 157 } 158 } 159 160 type exitError struct { 161 msg string 162 code int 163 } 164 165 func (e *exitError) Error() string { 166 return e.msg 167 }