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 }