github.com/connorvict/air@v0.0.0-20231005162537-279bf07db0d5/runner/config.go (about) 1 package runner 2 3 import ( 4 "flag" 5 "fmt" 6 "log" 7 "os" 8 "path/filepath" 9 "reflect" 10 "regexp" 11 "runtime" 12 "time" 13 14 "dario.cat/mergo" 15 "github.com/pelletier/go-toml" 16 ) 17 18 const ( 19 dftTOML = ".air.toml" 20 dftConf = ".air.conf" 21 airWd = "air_wd" 22 ) 23 24 // Config is the main configuration structure for Air. 25 type Config struct { 26 Root string `toml:"root"` 27 TmpDir string `toml:"tmp_dir"` 28 TestDataDir string `toml:"testdata_dir"` 29 Build cfgBuild `toml:"build"` 30 Color cfgColor `toml:"color"` 31 Log cfgLog `toml:"log"` 32 Misc cfgMisc `toml:"misc"` 33 Screen cfgScreen `toml:"screen"` 34 } 35 36 type cfgBuild struct { 37 PreCmd []string `toml:"pre_cmd"` 38 Cmd string `toml:"cmd"` 39 PostCmd []string `toml:"post_cmd"` 40 Bin string `toml:"bin"` 41 FullBin string `toml:"full_bin"` 42 ArgsBin []string `toml:"args_bin"` 43 Log string `toml:"log"` 44 IncludeExt []string `toml:"include_ext"` 45 ExcludeDir []string `toml:"exclude_dir"` 46 IncludeDir []string `toml:"include_dir"` 47 ExcludeFile []string `toml:"exclude_file"` 48 IncludeFile []string `toml:"include_file"` 49 ExcludeRegex []string `toml:"exclude_regex"` 50 ExcludeUnchanged bool `toml:"exclude_unchanged"` 51 FollowSymlink bool `toml:"follow_symlink"` 52 Poll bool `toml:"poll"` 53 PollInterval int `toml:"poll_interval"` 54 Delay int `toml:"delay"` 55 StopOnError bool `toml:"stop_on_error"` 56 SendInterrupt bool `toml:"send_interrupt"` 57 KillDelay time.Duration `toml:"kill_delay"` 58 Rerun bool `toml:"rerun"` 59 RerunDelay int `toml:"rerun_delay"` 60 regexCompiled []*regexp.Regexp 61 } 62 63 func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) { 64 if len(c.ExcludeRegex) > 0 && len(c.regexCompiled) == 0 { 65 c.regexCompiled = make([]*regexp.Regexp, 0, len(c.ExcludeRegex)) 66 for _, s := range c.ExcludeRegex { 67 re, err := regexp.Compile(s) 68 if err != nil { 69 return nil, err 70 } 71 c.regexCompiled = append(c.regexCompiled, re) 72 } 73 } 74 return c.regexCompiled, nil 75 } 76 77 type cfgLog struct { 78 AddTime bool `toml:"time"` 79 MainOnly bool `toml:"main_only"` 80 } 81 82 type cfgColor struct { 83 Main string `toml:"main"` 84 Watcher string `toml:"watcher"` 85 Build string `toml:"build"` 86 Runner string `toml:"runner"` 87 App string `toml:"app"` 88 } 89 90 type cfgMisc struct { 91 CleanOnExit bool `toml:"clean_on_exit"` 92 } 93 94 type cfgScreen struct { 95 ClearOnRebuild bool `toml:"clear_on_rebuild"` 96 KeepScroll bool `toml:"keep_scroll"` 97 } 98 99 type sliceTransformer struct{} 100 101 func (t sliceTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { 102 if typ.Kind() == reflect.Slice { 103 return func(dst, src reflect.Value) error { 104 if !src.IsZero() { 105 dst.Set(src) 106 } 107 return nil 108 } 109 } 110 return nil 111 } 112 113 // InitConfig initializes the configuration. 114 func InitConfig(path string) (cfg *Config, err error) { 115 if path == "" { 116 cfg, err = defaultPathConfig() 117 if err != nil { 118 return nil, err 119 } 120 } else { 121 cfg, err = readConfigOrDefault(path) 122 if err != nil { 123 return nil, err 124 } 125 } 126 config := defaultConfig() 127 // get addr 128 ret := &config 129 err = mergo.Merge(ret, cfg, func(config *mergo.Config) { 130 // mergo.Merge will overwrite the fields if it is Empty 131 // So need use this to avoid that none-zero slice will be overwritten. 132 // https://dario.cat/mergo#transformers 133 config.Transformers = sliceTransformer{} 134 config.Overwrite = true 135 }) 136 if err != nil { 137 return nil, err 138 } 139 140 err = ret.preprocess() 141 return ret, err 142 } 143 144 func writeDefaultConfig() { 145 confFiles := []string{dftTOML, dftConf} 146 147 for _, fname := range confFiles { 148 fstat, err := os.Stat(fname) 149 if err != nil && !os.IsNotExist(err) { 150 log.Fatal("failed to check for existing configuration") 151 return 152 } 153 if err == nil && fstat != nil { 154 log.Fatal("configuration already exists") 155 return 156 } 157 } 158 159 file, err := os.Create(dftTOML) 160 if err != nil { 161 log.Fatalf("failed to create a new configuration: %+v", err) 162 } 163 defer file.Close() 164 165 config := defaultConfig() 166 configFile, err := toml.Marshal(config) 167 if err != nil { 168 log.Fatalf("failed to marshal the default configuration: %+v", err) 169 } 170 171 _, err = file.Write(configFile) 172 if err != nil { 173 log.Fatalf("failed to write to %s: %+v", dftTOML, err) 174 } 175 176 fmt.Printf("%s file created to the current directory with the default settings\n", dftTOML) 177 } 178 179 func defaultPathConfig() (*Config, error) { 180 // when path is blank, first find `.air.toml`, `.air.conf` in `air_wd` and current working directory, if not found, use defaults 181 for _, name := range []string{dftTOML, dftConf} { 182 cfg, err := readConfByName(name) 183 if err == nil { 184 if name == dftConf { 185 fmt.Println("`.air.conf` will be deprecated soon, recommend using `.air.toml`.") 186 } 187 return cfg, nil 188 } 189 } 190 191 dftCfg := defaultConfig() 192 return &dftCfg, nil 193 } 194 195 func readConfByName(name string) (*Config, error) { 196 var path string 197 if wd := os.Getenv(airWd); wd != "" { 198 path = filepath.Join(wd, name) 199 } else { 200 wd, err := os.Getwd() 201 if err != nil { 202 return nil, err 203 } 204 path = filepath.Join(wd, name) 205 } 206 cfg, err := readConfig(path) 207 return cfg, err 208 } 209 210 func defaultConfig() Config { 211 build := cfgBuild{ 212 Cmd: "go build -o ./tmp/main .", 213 Bin: "./tmp/main", 214 Log: "build-errors.log", 215 IncludeExt: []string{"go", "tpl", "tmpl", "html"}, 216 IncludeDir: []string{}, 217 ExcludeFile: []string{}, 218 IncludeFile: []string{}, 219 ExcludeDir: []string{"assets", "tmp", "vendor", "testdata"}, 220 ArgsBin: []string{}, 221 ExcludeRegex: []string{"_test.go"}, 222 Delay: 0, 223 Rerun: false, 224 RerunDelay: 500, 225 } 226 if runtime.GOOS == PlatformWindows { 227 build.Bin = `tmp\main.exe` 228 build.Cmd = "go build -o ./tmp/main.exe ." 229 } 230 log := cfgLog{ 231 AddTime: false, 232 MainOnly: false, 233 } 234 color := cfgColor{ 235 Main: "magenta", 236 Watcher: "cyan", 237 Build: "yellow", 238 Runner: "green", 239 } 240 misc := cfgMisc{ 241 CleanOnExit: false, 242 } 243 return Config{ 244 Root: ".", 245 TmpDir: "tmp", 246 TestDataDir: "testdata", 247 Build: build, 248 Color: color, 249 Log: log, 250 Misc: misc, 251 Screen: cfgScreen{ 252 ClearOnRebuild: false, 253 KeepScroll: true, 254 }, 255 } 256 } 257 258 func readConfig(path string) (*Config, error) { 259 data, err := os.ReadFile(path) 260 if err != nil { 261 return nil, err 262 } 263 264 cfg := new(Config) 265 if err = toml.Unmarshal(data, cfg); err != nil { 266 return nil, err 267 } 268 269 return cfg, nil 270 } 271 272 func readConfigOrDefault(path string) (*Config, error) { 273 dftCfg := defaultConfig() 274 cfg, err := readConfig(path) 275 if err != nil { 276 return &dftCfg, err 277 } 278 279 return cfg, nil 280 } 281 282 func (c *Config) preprocess() error { 283 var err error 284 cwd := os.Getenv(airWd) 285 if cwd != "" { 286 if err = os.Chdir(cwd); err != nil { 287 return err 288 } 289 c.Root = cwd 290 } 291 c.Root, err = expandPath(c.Root) 292 if c.TmpDir == "" { 293 c.TmpDir = "tmp" 294 } 295 if c.TestDataDir == "" { 296 c.TestDataDir = "testdata" 297 } 298 if err != nil { 299 return err 300 } 301 ed := c.Build.ExcludeDir 302 for i := range ed { 303 ed[i] = cleanPath(ed[i]) 304 } 305 306 adaptToVariousPlatforms(c) 307 308 // Join runtime arguments with the configuration arguments 309 runtimeArgs := flag.Args() 310 c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...) 311 312 c.Build.ExcludeDir = ed 313 if len(c.Build.FullBin) > 0 { 314 c.Build.Bin = c.Build.FullBin 315 return err 316 } 317 // Fix windows CMD processor 318 // CMD will not recognize relative path like ./tmp/server 319 c.Build.Bin, err = filepath.Abs(c.Build.Bin) 320 321 return err 322 } 323 324 func (c *Config) colorInfo() map[string]string { 325 return map[string]string{ 326 "main": c.Color.Main, 327 "build": c.Color.Build, 328 "runner": c.Color.Runner, 329 "watcher": c.Color.Watcher, 330 } 331 } 332 333 func (c *Config) buildLogPath() string { 334 return filepath.Join(c.tmpPath(), c.Build.Log) 335 } 336 337 func (c *Config) buildDelay() time.Duration { 338 return time.Duration(c.Build.Delay) * time.Millisecond 339 } 340 341 func (c *Config) rerunDelay() time.Duration { 342 return time.Duration(c.Build.RerunDelay) * time.Millisecond 343 } 344 345 func (c *Config) binPath() string { 346 return filepath.Join(c.Root, c.Build.Bin) 347 } 348 349 func (c *Config) tmpPath() string { 350 return filepath.Join(c.Root, c.TmpDir) 351 } 352 353 func (c *Config) testDataPath() string { 354 return filepath.Join(c.Root, c.TestDataDir) 355 } 356 357 func (c *Config) rel(path string) string { 358 s, err := filepath.Rel(c.Root, path) 359 if err != nil { 360 return "" 361 } 362 return s 363 } 364 365 // WithArgs returns a new config with the given arguments added to the configuration. 366 func (c *Config) WithArgs(args map[string]TomlInfo) { 367 for _, value := range args { 368 if value.Value != nil && *value.Value != unsetDefault { 369 v := reflect.ValueOf(c) 370 setValue2Struct(v, value.fieldPath, *value.Value) 371 } 372 } 373 }