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 }