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  }