github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/tiltfile/config/config_def.go (about) 1 package config 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "strings" 8 9 jsoniter "github.com/json-iterator/go" 10 "github.com/pkg/errors" 11 flag "github.com/spf13/pflag" 12 "go.starlark.net/starlark" 13 14 "github.com/tilt-dev/tilt/internal/tiltfile/starkit" 15 ) 16 17 type configValue interface { 18 flag.Value 19 starlark() starlark.Value 20 setFromInterface(interface{}) error 21 IsSet() bool 22 } 23 24 type configMap map[string]configValue 25 26 type configSetting struct { 27 newValue func() configValue 28 usage string 29 } 30 31 type ConfigDef struct { 32 positionalSettingName string 33 configSettings map[string]configSetting 34 } 35 36 func (cm configMap) toStarlark() (starlark.Mapping, error) { 37 ret := starlark.NewDict(len(cm)) 38 for k, v := range cm { 39 err := ret.SetKey(starlark.String(k), v.starlark()) 40 if err != nil { 41 return nil, err 42 } 43 } 44 return ret, nil 45 } 46 47 // merges settings from config and settings from args, with settings from args trumping 48 func mergeConfigMaps(settingsFromConfig, settingsFromArgs configMap) configMap { 49 ret := make(configMap) 50 for k, v := range settingsFromConfig { 51 ret[k] = v 52 } 53 54 for k, v := range settingsFromArgs { 55 if v.IsSet() { 56 ret[k] = v 57 } 58 } 59 60 return ret 61 } 62 63 // parse any args and merge them into the config 64 func (cd ConfigDef) incorporateArgs(config configMap, args []string) (ret configMap, output string, err error) { 65 var settingsFromArgs configMap 66 settingsFromArgs, output, err = cd.parseArgs(args) 67 if err != nil { 68 return nil, output, fmt.Errorf("invalid Tiltfile config args: %v", err) 69 } 70 71 config = mergeConfigMaps(config, settingsFromArgs) 72 73 return config, output, nil 74 } 75 76 func (cd ConfigDef) parse(configPath string, args []string) (v starlark.Value, output string, err error) { 77 config, err := cd.readFromFile(configPath) 78 if err != nil { 79 return starlark.None, "", err 80 } 81 82 config, output, err = cd.incorporateArgs(config, args) 83 if err != nil { 84 return starlark.None, output, err 85 } 86 87 ret, err := config.toStarlark() 88 if err != nil { 89 return nil, output, err 90 } 91 92 return ret, output, nil 93 } 94 95 // parse command-line args 96 func (cd ConfigDef) parseArgs(args []string) (ret configMap, output string, err error) { 97 fs := flag.NewFlagSet("", flag.ContinueOnError) 98 w := &bytes.Buffer{} 99 fs.SetOutput(w) 100 101 ret = make(configMap) 102 for name, def := range cd.configSettings { 103 ret[name] = def.newValue() 104 if name == cd.positionalSettingName { 105 continue 106 } 107 fs.Var(ret[name], name, def.usage) 108 // for bools, make "--foo" equal to "--foo true" 109 if _, ok := ret[name].(*boolSetting); ok { 110 fs.Lookup(name).NoOptDefVal = "true" 111 } 112 } 113 114 err = fs.Parse(args) 115 if err != nil { 116 usage := fs.FlagUsagesWrapped(80) 117 if strings.TrimSpace(usage) != "" { 118 usage = "\nUsage:\n" + usage 119 } 120 return nil, w.String(), fmt.Errorf("%v%s", err, usage) 121 } 122 123 if len(fs.Args()) > 0 { 124 if cd.positionalSettingName == "" { 125 return nil, w.String(), fmt.Errorf( 126 "positional CLI args (%q) were specified, but none were expected.\n"+ 127 "See https://docs.tilt.dev/tiltfile_config.html#positional-arguments for examples.", strings.Join(fs.Args(), " ")) 128 } else { 129 for _, arg := range fs.Args() { 130 err := ret[cd.positionalSettingName].Set(arg) 131 if err != nil { 132 return nil, w.String(), fmt.Errorf("invalid positional args (%s): %v", cd.positionalSettingName, err) 133 } 134 } 135 } 136 } 137 138 return ret, w.String(), nil 139 } 140 141 // parse settings from the config file 142 func (cd ConfigDef) readFromFile(tiltConfigPath string) (ret configMap, err error) { 143 ret = make(configMap) 144 r, err := os.Open(tiltConfigPath) 145 if err != nil { 146 if os.IsNotExist(err) { 147 return ret, nil 148 } 149 return nil, errors.Wrapf(err, "error opening %s", tiltConfigPath) 150 } 151 defer func() { 152 _ = r.Close() 153 }() 154 155 m := make(map[string]interface{}) 156 err = jsoniter.NewDecoder(r).Decode(&m) 157 if err != nil { 158 return nil, errors.Wrapf(err, "error parsing json from %s", tiltConfigPath) 159 } 160 161 for k, v := range m { 162 def, ok := cd.configSettings[k] 163 if !ok { 164 return nil, fmt.Errorf("%s specified unknown setting name '%s'", tiltConfigPath, k) 165 } 166 ret[k] = def.newValue() 167 err = ret[k].setFromInterface(v) 168 if err != nil { 169 return nil, errors.Wrapf(err, "%s specified invalid value for setting %s", tiltConfigPath, k) 170 } 171 } 172 return ret, nil 173 } 174 175 // makes a new builtin with the given configValue constructor 176 // newConfigValue: a constructor for the `configValue` that we're making a function for 177 // 178 // (it's the same logic for all types, except for the `configValue` that gets saved) 179 func configSettingDefinitionBuiltin(newConfigValue func() configValue) starkit.Function { 180 return func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 181 var name string 182 var isArgs bool 183 var usage string 184 err := starkit.UnpackArgs(thread, fn.Name(), args, kwargs, 185 "name", 186 &name, 187 "args?", 188 &isArgs, 189 "usage?", 190 &usage, 191 ) 192 if err != nil { 193 return starlark.None, err 194 } 195 196 if name == "" { 197 return starlark.None, errors.New("'name' is required") 198 } 199 200 err = starkit.SetState(thread, func(settings Settings) (Settings, error) { 201 if settings.configParseCalled { 202 return settings, fmt.Errorf("%s cannot be called after config.parse is called", fn.Name()) 203 } 204 205 if _, ok := settings.configDef.configSettings[name]; ok { 206 return settings, fmt.Errorf("%s defined multiple times", name) 207 } 208 209 if isArgs { 210 if settings.configDef.positionalSettingName != "" { 211 return settings, fmt.Errorf("both %s and %s are defined as positional args", name, settings.configDef.positionalSettingName) 212 } 213 214 settings.configDef.positionalSettingName = name 215 } 216 217 settings.configDef.configSettings[name] = configSetting{ 218 newValue: newConfigValue, 219 usage: usage, 220 } 221 222 return settings, nil 223 }) 224 if err != nil { 225 return starlark.None, err 226 } 227 228 return starlark.None, nil 229 } 230 }