github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/value/value.go (about) 1 package value 2 3 import ( 4 "fmt" 5 "math" 6 "runtime" 7 "sort" 8 9 "github.com/pkg/errors" 10 "go.starlark.net/starlark" 11 12 "github.com/tilt-dev/tilt/internal/tiltfile/starkit" 13 "github.com/tilt-dev/tilt/pkg/model" 14 ) 15 16 // If `v` is a `starlark.Sequence`, return a slice of its elements 17 // Otherwise, return it as a single-element slice 18 // For functions that take `Union[List[T], T]` 19 func ValueOrSequenceToSlice(v starlark.Value) []starlark.Value { 20 if seq, ok := v.(starlark.Sequence); ok { 21 var ret []starlark.Value 22 it := seq.Iterate() 23 defer it.Done() 24 var i starlark.Value 25 for it.Next(&i) { 26 ret = append(ret, i) 27 } 28 return ret 29 } else if v == nil || v == starlark.None { 30 return nil 31 } else { 32 return []starlark.Value{v} 33 } 34 } 35 36 func ValueToAbsPath(thread *starlark.Thread, v starlark.Value) (string, error) { 37 pathMaker, ok := v.(PathMaker) 38 if ok { 39 return pathMaker.MakeLocalPath("."), nil 40 } 41 42 str, ok := starlark.AsString(v) 43 if ok { 44 return starkit.AbsPath(thread, str), nil 45 } 46 47 return "", fmt.Errorf("expected path | string. Actual type: %T", v) 48 } 49 50 type PathMaker interface { 51 MakeLocalPath(relPath string) string 52 } 53 54 // StringSequence is a convenience type for dealing with string slices in Starlark. 55 type StringSequence []string 56 57 func (s StringSequence) Sequence() starlark.Sequence { 58 elems := make([]starlark.Value, 0, len(s)) 59 for _, v := range s { 60 elems = append(elems, starlark.String(v)) 61 } 62 return starlark.NewList(elems) 63 } 64 65 func (s *StringSequence) Unpack(v starlark.Value) error { 66 if v == nil { 67 *s = nil 68 return nil 69 } 70 seq, ok := v.(starlark.Sequence) 71 if !ok { 72 return fmt.Errorf("'%v' is a %T, not a sequence", v, v) 73 } 74 out, err := SequenceToStringSlice(seq) 75 if err != nil { 76 return err 77 } 78 *s = out 79 return nil 80 } 81 82 func SequenceToStringSlice(seq starlark.Sequence) ([]string, error) { 83 if seq == nil || seq.Len() == 0 { 84 return nil, nil 85 } 86 it := seq.Iterate() 87 defer it.Done() 88 ret := make([]string, 0, seq.Len()) 89 var v starlark.Value 90 for it.Next(&v) { 91 s, ok := v.(starlark.String) 92 if !ok { 93 return nil, fmt.Errorf("'%v' is a %T, not a string", v, v) 94 } 95 ret = append(ret, string(s)) 96 } 97 return ret, nil 98 } 99 100 func StringSliceToList(slice []string) *starlark.List { 101 v := []starlark.Value{} 102 for _, s := range slice { 103 v = append(v, starlark.String(s)) 104 } 105 return starlark.NewList(v) 106 } 107 108 // In other similar build systems (Buck and Bazel), 109 // there's a "main" command, and then various per-platform overrides. 110 // https://docs.bazel.build/versions/master/be/general.html#genrule.cmd_bat 111 // This helper function abstracts out the precedence rules. 112 func ValueGroupToCmdHelper(t *starlark.Thread, cmdVal, cmdBatVal, cmdDir starlark.Value, env map[string]string) (model.Cmd, error) { 113 if cmdBatVal != nil && runtime.GOOS == "windows" { 114 return ValueToBatCmd(t, cmdBatVal, cmdDir, env) 115 } 116 return ValueToHostCmd(t, cmdVal, cmdDir, env) 117 } 118 119 // provides dockerfile-style behavior of: 120 // a string gets interpreted as a shell command (like, sh -c 'foo bar $X') 121 // an array of strings gets interpreted as a raw argv to exec 122 func ValueToHostCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) { 123 return valueToCmdHelper(t, v, dir, env, model.ToHostCmd) 124 } 125 126 func ValueToBatCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) { 127 return valueToCmdHelper(t, v, dir, env, model.ToBatCmd) 128 } 129 130 func ValueToUnixCmd(t *starlark.Thread, v, dir starlark.Value, env map[string]string) (model.Cmd, error) { 131 return valueToCmdHelper(t, v, dir, env, model.ToUnixCmd) 132 } 133 134 func valueToCmdHelper(t *starlark.Thread, cmdVal, cmdDirVal starlark.Value, cmdEnv map[string]string, stringToCmd func(string) model.Cmd) (model.Cmd, error) { 135 136 var dir string 137 var dirErr error 138 139 switch cmdDirVal.(type) { 140 case nil: 141 dir = starkit.AbsWorkingDir(t) 142 case starlark.NoneType: 143 dir = starkit.AbsWorkingDir(t) 144 default: 145 dir, dirErr = ValueToAbsPath(t, cmdDirVal) 146 if dirErr != nil { 147 return model.Cmd{}, errors.Wrap(dirErr, "a command directory must be empty or a string") 148 } 149 } 150 151 env, err := envTuples(cmdEnv) 152 if err != nil { 153 return model.Cmd{}, err 154 } 155 156 switch x := cmdVal.(type) { 157 // If a starlark function takes an optional command argument, then UnpackArgs will set its starlark.Value to nil 158 // we convert nils here to an empty Cmd, since otherwise every callsite would have to do a nil check with presumably 159 // the same outcome 160 case nil: 161 return model.Cmd{}, nil 162 case starlark.String: 163 cmd := stringToCmd(string(x)) 164 cmd.Dir = dir 165 cmd.Env = env 166 return cmd, nil 167 case starlark.Sequence: 168 argv, err := SequenceToStringSlice(x) 169 if err != nil { 170 return model.Cmd{}, errors.Wrap(err, "a command must be a string or a list of strings") 171 } 172 return model.Cmd{Argv: argv, Dir: dir, Env: env}, nil 173 default: 174 return model.Cmd{}, fmt.Errorf("a command must be a string or list of strings. found %T", x) 175 } 176 } 177 178 func envTuples(env map[string]string) ([]string, error) { 179 var kv []string 180 for k, v := range env { 181 if k == "" { 182 return nil, fmt.Errorf("environment has empty key for value %q", v) 183 } 184 kv = append(kv, k+"="+v) 185 } 186 // sorting here is for consistency/predictability; since the input is a map, uniqueness 187 // is guaranteed so order is not actually relevant, but this simplifies usage in tests, 188 // for example 189 sort.Strings(kv) 190 return kv, nil 191 } 192 193 // Int32 is a convenience type for unpacking int32 bounded values. 194 type Int32 struct { 195 starlark.Int 196 } 197 198 // Int32 returns the value as an int32. 199 // 200 // It will panic if the value cannot be accurate represented as an int32. 201 func (i Int32) Int32() int32 { 202 v, err := starlarkIntAsInt32(i.Int) 203 if err != nil { 204 // bounds check should have happened during unpacking, so something 205 // is very wrong if we get here 206 panic(err) 207 } 208 return v 209 } 210 211 func (i *Int32) Unpack(v starlark.Value) error { 212 if v == nil { 213 return fmt.Errorf("got %s, want int", starlark.None.Type()) 214 } 215 x, ok := v.(starlark.Int) 216 if !ok { 217 return fmt.Errorf("got %s, want int", v.Type()) 218 } 219 if _, err := starlarkIntAsInt32(x); err != nil { 220 return err 221 } 222 i.Int = x 223 return nil 224 } 225 226 func starlarkIntAsInt32(v starlark.Int) (int32, error) { 227 x, ok := v.Int64() 228 if !ok || x < math.MinInt32 || x > math.MaxInt32 { 229 return 0, fmt.Errorf("value out of range for int32: %s", v.String()) 230 } 231 return int32(x), nil 232 }