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

     1  package io
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/pkg/errors"
    10  	"go.starlark.net/starlark"
    11  
    12  	"github.com/tilt-dev/tilt/internal/sliceutils"
    13  	"github.com/tilt-dev/tilt/internal/tiltfile/starkit"
    14  	"github.com/tilt-dev/tilt/internal/tiltfile/value"
    15  )
    16  
    17  type WatchType int
    18  
    19  const (
    20  	// If it's a file, only watch the file. If it's a directory, don't watch at all.
    21  	WatchFileOnly WatchType = iota
    22  
    23  	// If it's a file, only watch the file. If it's a directory, watch it recursively.
    24  	WatchRecursive
    25  )
    26  
    27  type Plugin struct{}
    28  
    29  func NewPlugin() Plugin {
    30  	return Plugin{}
    31  }
    32  
    33  func (Plugin) NewState() interface{} {
    34  	return ReadState{}
    35  }
    36  
    37  func (Plugin) OnStart(e *starkit.Environment) error {
    38  	err := e.AddBuiltin("read_file", readFile)
    39  	if err != nil {
    40  		return err
    41  	}
    42  
    43  	err = e.AddBuiltin("watch_file", watchFile)
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	err = e.AddBuiltin("listdir", listdir)
    49  	if err != nil {
    50  		return err
    51  	}
    52  
    53  	err = e.AddBuiltin("blob", blob)
    54  	if err != nil {
    55  		return err
    56  	}
    57  
    58  	return nil
    59  }
    60  
    61  func (Plugin) OnExec(t *starlark.Thread, tiltfilePath string, contents []byte) error {
    62  	return RecordReadPath(t, WatchFileOnly, tiltfilePath)
    63  }
    64  
    65  func readFile(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    66  	path := value.NewLocalPathUnpacker(thread)
    67  	var defaultReturnValue value.Optional[starlark.String]
    68  	err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs, "paths", &path, "default?", &defaultReturnValue)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	p := path.Value
    74  	bs, err := ReadFile(thread, p)
    75  	if os.IsNotExist(err) && defaultReturnValue.IsSet {
    76  		bs = []byte(defaultReturnValue.Value)
    77  	} else if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	return NewBlob(string(bs), fmt.Sprintf("file: %s", p)), nil
    82  }
    83  
    84  func watchFile(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
    85  	path := value.NewLocalPathUnpacker(thread)
    86  	err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs, "paths", &path)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	p := path.Value
    92  	err = RecordReadPath(thread, WatchRecursive, p)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	return starlark.None, nil
    98  }
    99  
   100  func listdir(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   101  	dir := value.NewLocalPathUnpacker(thread)
   102  	var recursive bool
   103  	err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs, "dir", &dir, "recursive?", &recursive)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	localPath := dir.Value
   109  
   110  	// We currently don't watch the directory only, because Tilt doesn't have any
   111  	// way to watch a directory without watching it recursively.
   112  	if recursive {
   113  		err = RecordReadPath(thread, WatchRecursive, localPath)
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  	}
   118  
   119  	var files []string
   120  	err = filepath.WalkDir(localPath, func(path string, info fs.DirEntry, err error) error {
   121  		if path == localPath {
   122  			return nil
   123  		}
   124  		if !info.IsDir() {
   125  			files = append(files, path)
   126  		} else if info.IsDir() && !recursive {
   127  			return filepath.SkipDir
   128  		}
   129  		return nil
   130  	})
   131  	if err != nil {
   132  		return nil, err
   133  	}
   134  
   135  	var ret []starlark.Value
   136  	for _, f := range files {
   137  		ret = append(ret, starlark.String(f))
   138  	}
   139  
   140  	return starlark.NewList(ret), nil
   141  }
   142  
   143  func blob(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
   144  	var input starlark.String
   145  	err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs, "input", &input)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	return NewBlob(input.GoString(), "Tiltfile blob() call"), nil
   151  }
   152  
   153  // Track all the paths read while loading
   154  type ReadState struct {
   155  	Paths []string
   156  }
   157  
   158  func ReadFile(thread *starlark.Thread, p string) ([]byte, error) {
   159  	err := RecordReadPath(thread, WatchFileOnly, p)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  	return os.ReadFile(p)
   164  }
   165  
   166  func RecordReadPath(t *starlark.Thread, wt WatchType, files ...string) error {
   167  	toWatch := make([]string, 0, len(files))
   168  	for _, f := range files {
   169  		switch wt {
   170  		case WatchRecursive:
   171  			toWatch = append(toWatch, f)
   172  
   173  		case WatchFileOnly:
   174  			info, err := os.Lstat(f)
   175  			shouldWatch := false
   176  			if os.IsNotExist(err) {
   177  				// If a file does not exist, we should watch the space
   178  				// to see if the file does appear.
   179  				shouldWatch = true
   180  			} else if err != nil {
   181  				// If we got a permission denied error, we should stop.
   182  				return err
   183  			} else if !info.IsDir() {
   184  				// Tilt only knows how to do recursive watches. If we read a directory
   185  				// during Tiltfile execution, we'd rather not watch the directory at all
   186  				// rather than overwatch and over-trigger Tiltfile reloads.
   187  				//
   188  				// https://github.com/tilt-dev/tilt/issues/3387
   189  				shouldWatch = true
   190  			}
   191  
   192  			if shouldWatch {
   193  				toWatch = append(toWatch, f)
   194  			}
   195  
   196  		default:
   197  			return fmt.Errorf("Unknown watch type: %v", t)
   198  		}
   199  	}
   200  
   201  	err := starkit.SetState(t, func(s ReadState) ReadState {
   202  		s.Paths = sliceutils.AppendWithoutDupes(s.Paths, toWatch...)
   203  		return s
   204  	})
   205  	return errors.Wrap(err, "error recording read file")
   206  }
   207  
   208  var _ starkit.StatefulPlugin = Plugin{}
   209  var _ starkit.OnExecPlugin = Plugin{}
   210  
   211  func MustState(model starkit.Model) ReadState {
   212  	state, err := GetState(model)
   213  	if err != nil {
   214  		panic(err)
   215  	}
   216  	return state
   217  }
   218  
   219  func GetState(m starkit.Model) (ReadState, error) {
   220  	var state ReadState
   221  	err := m.Load(&state)
   222  	return state, err
   223  }