github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/localexec/localexec.go (about)

     1  // Package localexec provides constructs for uniform execution of local processes,
     2  // specifically conversion from model.Cmd to exec.Cmd.
     3  package localexec
     4  
     5  import (
     6  	"errors"
     7  	"os"
     8  	"os/exec"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/tilt-dev/tilt/pkg/logger"
    13  	"github.com/tilt-dev/tilt/pkg/model"
    14  )
    15  
    16  // Common environment for local exec commands.
    17  type Env struct {
    18  	pairs   []kvPair
    19  	environ func() []string
    20  }
    21  
    22  func EmptyEnv() *Env {
    23  	return &Env{
    24  		environ: os.Environ,
    25  	}
    26  }
    27  
    28  func DefaultEnv(port model.WebPort, host model.WebHost) *Env {
    29  	e := &Env{
    30  		environ: os.Environ,
    31  	}
    32  
    33  	// if Tilt was invoked with `tilt up --port=XXXXX`, local() calls to use the Tilt API will fail due to trying to
    34  	// connect to the default port, so explicitly populate the TILT_PORT environment variable if it isn't already
    35  	e.Add("TILT_PORT", strconv.Itoa(int(port)))
    36  
    37  	// some Tilt commands, such as `tilt dump engine`, also require the host
    38  	e.Add("TILT_HOST", string(host))
    39  
    40  	// We don't really want to collect analytics when extensions use 'tilt get'/'tilt apply'.
    41  	e.Add("TILT_DISABLE_ANALYTICS", "1")
    42  
    43  	return e
    44  }
    45  
    46  func (e *Env) Add(k, v string) {
    47  	e.pairs = append(e.pairs, kvPair{Key: k, Value: v})
    48  }
    49  
    50  // ExecCmd creates a stdlib exec.Cmd instance suitable for execution by the local engine.
    51  //
    52  // The resulting command will inherit the parent process (i.e. `tilt`) environment, then
    53  // have command specific environment overrides applied, and finally, additional conditional
    54  // environment to improve logging output.
    55  //
    56  // NOTE: To avoid confusion with ExecCmdContext, this method accepts a logger instance
    57  // directly rather than using logger.Get(ctx); the returned exec.Cmd from this function
    58  // will NOT be associated with any context.
    59  func (e *Env) ExecCmd(cmd model.Cmd, l logger.Logger) (*exec.Cmd, error) {
    60  	if len(cmd.Argv) == 0 {
    61  		return nil, errors.New("empty cmd")
    62  	}
    63  	c := exec.Command(cmd.Argv[0], cmd.Argv[1:]...)
    64  	e.populateExecCmd(c, cmd, l)
    65  	return c, nil
    66  }
    67  
    68  func (e *Env) populateExecCmd(c *exec.Cmd, cmd model.Cmd, l logger.Logger) {
    69  	c.Dir = cmd.Dir
    70  	// env precedence: parent process (i.e. tilt) -> logger -> command
    71  	// dupes are left for Go stdlib to handle (API guarantees last wins)
    72  	execEnv := e.environ()
    73  
    74  	execEnv = logger.PrepareEnv(l, execEnv)
    75  	for _, kv := range e.pairs {
    76  		execEnv = addEnvIfNotPresent(execEnv, kv.Key, kv.Value)
    77  	}
    78  
    79  	execEnv = append(execEnv, cmd.Env...)
    80  	c.Env = execEnv
    81  }
    82  
    83  type kvPair struct {
    84  	Key   string
    85  	Value string
    86  }
    87  
    88  func addEnvIfNotPresent(env []string, key, value string) []string {
    89  	prefix := key + "="
    90  	for _, e := range env {
    91  		if strings.HasPrefix(e, prefix) {
    92  			return env
    93  		}
    94  	}
    95  
    96  	return append(env, key+"="+value)
    97  }