github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/starkit/environment.go (about) 1 package starkit 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "github.com/pkg/errors" 12 "go.starlark.net/resolve" 13 "go.starlark.net/starlark" 14 15 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 16 ) 17 18 func init() { 19 resolve.AllowSet = true 20 resolve.AllowLambda = true 21 resolve.AllowNestedDef = true 22 resolve.AllowGlobalReassign = true 23 resolve.AllowRecursion = true 24 } 25 26 // The main entrypoint to starkit. 27 // Execute a file with a set of starlark plugins. 28 func ExecFile(tf *v1alpha1.Tiltfile, plugins ...Plugin) (Model, error) { 29 return newEnvironment(plugins...).start(tf) 30 } 31 32 const argUnpackerKey = "starkit.ArgUnpacker" 33 const modelKey = "starkit.Model" 34 const ctxKey = "starkit.Ctx" 35 const startTfKey = "starkit.StartTiltfile" 36 const execingTiltfileKey = "starkit.ExecingTiltfile" 37 38 // Unpacks args, using the arg unpacker on the current thread. 39 func UnpackArgs(t *starlark.Thread, fnName string, args starlark.Tuple, kwargs []starlark.Tuple, pairs ...interface{}) error { 40 unpacker, ok := t.Local(argUnpackerKey).(ArgUnpacker) 41 if !ok { 42 return starlark.UnpackArgs(fnName, args, kwargs, pairs...) 43 } 44 return unpacker(fnName, args, kwargs, pairs...) 45 } 46 47 type BuiltinCall struct { 48 Name string 49 Args starlark.Tuple 50 Dur time.Duration 51 } 52 53 // A starlark execution environment. 54 type Environment struct { 55 ctx context.Context 56 startTf *v1alpha1.Tiltfile 57 unpackArgs ArgUnpacker 58 loadCache map[string]loadCacheEntry 59 predeclared starlark.StringDict 60 print func(thread *starlark.Thread, msg string) 61 plugins []Plugin 62 fakeFileSystem map[string]string 63 loadInterceptors []LoadInterceptor 64 65 builtinCalls []BuiltinCall 66 } 67 68 func NewThread(ctx context.Context, model Model) *starlark.Thread { 69 t := &starlark.Thread{} 70 t.SetLocal(modelKey, model) 71 t.SetLocal(ctxKey, ctx) 72 return t 73 } 74 75 func newEnvironment(plugins ...Plugin) *Environment { 76 return &Environment{ 77 unpackArgs: starlark.UnpackArgs, 78 loadCache: make(map[string]loadCacheEntry), 79 plugins: append([]Plugin{}, plugins...), 80 predeclared: starlark.StringDict{}, 81 fakeFileSystem: nil, 82 builtinCalls: []BuiltinCall{}, 83 } 84 } 85 86 func (e *Environment) AddLoadInterceptor(i LoadInterceptor) { 87 e.loadInterceptors = append(e.loadInterceptors, i) 88 } 89 90 func (e *Environment) SetArgUnpacker(unpackArgs ArgUnpacker) { 91 e.unpackArgs = unpackArgs 92 } 93 94 // The tiltfile model driving this environment. 95 func (e *Environment) StartTiltfile() *v1alpha1.Tiltfile { 96 return e.startTf 97 } 98 99 // Add a builtin to the environment. 100 // 101 // All builtins will be wrapped to invoke OnBuiltinCall on every plugin. 102 // 103 // All builtins should use starkit.UnpackArgs to get instrumentation. 104 func (e *Environment) AddBuiltin(name string, f Function) error { 105 wrapped := starlark.NewBuiltin(name, func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 106 for _, ext := range e.plugins { 107 onBuiltinCallExt, ok := ext.(OnBuiltinCallPlugin) 108 if ok { 109 onBuiltinCallExt.OnBuiltinCall(name, fn) 110 } 111 } 112 113 start := time.Now() 114 defer func() { 115 e.builtinCalls = append(e.builtinCalls, BuiltinCall{ 116 Name: name, 117 Args: args, 118 Dur: time.Since(start), 119 }) 120 }() 121 return f(thread, fn, args, kwargs) 122 }) 123 124 return e.AddValue(name, wrapped) 125 } 126 127 func (e *Environment) AddValue(name string, val starlark.Value) error { 128 split := strings.Split(name, ".") 129 130 var attrMap = e.predeclared 131 132 // Iterate thru the module tree. 133 for i := 0; i < len(split)-1; i++ { 134 var currentModule Module 135 currentPart := split[i] 136 fullName := strings.Join(split[:i+1], ".") 137 predeclaredVal, ok := attrMap[currentPart] 138 if ok { 139 predeclaredDict, ok := predeclaredVal.(Module) 140 if !ok { 141 return fmt.Errorf("Module conflict at %s. Existing: %s", fullName, predeclaredVal) 142 } 143 currentModule = predeclaredDict 144 } else { 145 currentModule = Module{fullName: fullName, attrs: starlark.StringDict{}} 146 attrMap[currentPart] = currentModule 147 } 148 149 attrMap = currentModule.attrs 150 } 151 152 baseName := split[len(split)-1] 153 if _, ok := attrMap[baseName]; ok { 154 return fmt.Errorf("multiple values added named %s", name) 155 } 156 attrMap[baseName] = val 157 return nil 158 } 159 160 func (e *Environment) SetPrint(print func(thread *starlark.Thread, msg string)) { 161 e.print = print 162 } 163 164 func (e *Environment) SetContext(ctx context.Context) { 165 e.ctx = ctx 166 } 167 168 // Set a fake file system so that we can write tests that don't 169 // touch the file system. Expressed as a map from paths to contents. 170 func (e *Environment) SetFakeFileSystem(files map[string]string) { 171 e.fakeFileSystem = files 172 } 173 174 func (e *Environment) newThread(model Model) *starlark.Thread { 175 t := NewThread(e.ctx, model) 176 t.Load = e.load 177 t.Print = e.print 178 t.SetLocal(argUnpackerKey, e.unpackArgs) 179 t.SetLocal(startTfKey, e.startTf) 180 return t 181 } 182 183 func (e *Environment) start(tf *v1alpha1.Tiltfile) (Model, error) { 184 // NOTE(dmiller): we only call Abs here because it's the root of the stack 185 path, err := filepath.Abs(tf.Spec.Path) 186 if err != nil { 187 return Model{}, errors.Wrap(err, "environment#start") 188 } 189 190 e.startTf = tf 191 192 model, err := NewModel(e.plugins...) 193 if err != nil { 194 return Model{}, err 195 } 196 197 for _, ext := range e.plugins { 198 err := ext.OnStart(e) 199 if err != nil { 200 return Model{}, errors.Wrapf(err, "internal error: %T", ext) 201 } 202 } 203 204 t := e.newThread(model) 205 _, err = e.exec(t, path) 206 model.BuiltinCalls = e.builtinCalls 207 if errors.Is(err, ErrStopExecution) { 208 return model, nil 209 } 210 return model, err 211 } 212 213 func (e *Environment) load(t *starlark.Thread, path string) (starlark.StringDict, error) { 214 return e.exec(t, path) 215 } 216 217 func (e *Environment) exec(t *starlark.Thread, path string) (starlark.StringDict, error) { 218 localPath, err := e.getPath(t, path) 219 if err != nil { 220 e.loadCache[localPath] = loadCacheEntry{ 221 status: loadStatusDone, 222 exports: starlark.StringDict{}, 223 err: err, 224 } 225 return starlark.StringDict{}, err 226 } 227 228 entry := e.loadCache[localPath] 229 switch entry.status { 230 case loadStatusExecuting: 231 return starlark.StringDict{}, fmt.Errorf("Circular load: %s", localPath) 232 case loadStatusDone: 233 return entry.exports, entry.err 234 } 235 236 e.loadCache[localPath] = loadCacheEntry{ 237 status: loadStatusExecuting, 238 } 239 240 oldPath := t.Local(execingTiltfileKey) 241 t.SetLocal(execingTiltfileKey, localPath) 242 243 exports, err := e.doLoad(t, localPath) 244 245 t.SetLocal(execingTiltfileKey, oldPath) 246 247 e.loadCache[localPath] = loadCacheEntry{ 248 status: loadStatusDone, 249 exports: exports, 250 err: err, 251 } 252 return exports, err 253 } 254 255 func (e *Environment) getPath(t *starlark.Thread, path string) (string, error) { 256 for _, i := range e.loadInterceptors { 257 newPath, err := i.LocalPath(t, path) 258 if err != nil { 259 return "", err 260 } 261 if newPath != "" { 262 // we found an interceptor that does something with this path, return early 263 return newPath, nil 264 } 265 } 266 267 return AbsPath(t, path), nil 268 } 269 270 func (e *Environment) doLoad(t *starlark.Thread, localPath string) (starlark.StringDict, error) { 271 var bytes []byte 272 if e.fakeFileSystem != nil { 273 contents, ok := e.fakeFileSystem[localPath] 274 if !ok { 275 return starlark.StringDict{}, fmt.Errorf("Not in fake file system: %s", localPath) 276 } 277 bytes = []byte(contents) 278 } else { 279 var err error 280 bytes, err = os.ReadFile(localPath) 281 if err != nil { 282 return starlark.StringDict{}, fmt.Errorf("error reading file %s: %w", localPath, err) 283 } 284 } 285 286 for _, ext := range e.plugins { 287 onExecExt, ok := ext.(OnExecPlugin) 288 if ok { 289 err := onExecExt.OnExec(t, localPath, bytes) 290 if err != nil { 291 return starlark.StringDict{}, err 292 } 293 } 294 } 295 296 // Create a copy of predeclared variables so we can specify Tiltfile-specific values. 297 predeclared := starlark.StringDict{} 298 for k, v := range e.predeclared { 299 predeclared[k] = v 300 } 301 predeclared["__file__"] = starlark.String(localPath) 302 303 return starlark.ExecFile(t, localPath, bytes, predeclared) 304 } 305 306 type ArgUnpacker func(fnName string, args starlark.Tuple, kwargs []starlark.Tuple, pairs ...interface{}) error 307 308 const ( 309 loadStatusNone loadStatus = iota 310 loadStatusExecuting 311 loadStatusDone 312 ) 313 314 var _ loadStatus = loadStatusNone 315 316 type loadCacheEntry struct { 317 status loadStatus 318 exports starlark.StringDict 319 err error 320 } 321 322 type loadStatus int