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