github.com/zhb127/air@v0.0.2-0.20231109030911-fb911e430cdd/runner/config.go (about) 1 package runner 2 3 import ( 4 "errors" 5 "flag" 6 "fmt" 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() (string, error) { 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 return "", fmt.Errorf("failed to check for existing configuration: %w", err) 151 } 152 if err == nil && fstat != nil { 153 return "", errors.New("configuration already exists") 154 } 155 } 156 157 file, err := os.Create(dftTOML) 158 if err != nil { 159 return "", fmt.Errorf("failed to create a new configuration: %w", err) 160 } 161 defer file.Close() 162 163 config := defaultConfig() 164 configFile, err := toml.Marshal(config) 165 if err != nil { 166 return "", fmt.Errorf("failed to marshal the default configuration: %w", err) 167 } 168 169 _, err = file.Write(configFile) 170 if err != nil { 171 return "", fmt.Errorf("failed to write to %s: %w", dftTOML, err) 172 } 173 174 return dftTOML, nil 175 } 176 177 func defaultPathConfig() (*Config, error) { 178 // when path is blank, first find `.air.toml`, `.air.conf` in `air_wd` and current working directory, if not found, use defaults 179 for _, name := range []string{dftTOML, dftConf} { 180 cfg, err := readConfByName(name) 181 if err == nil { 182 if name == dftConf { 183 fmt.Println("`.air.conf` will be deprecated soon, recommend using `.air.toml`.") 184 } 185 return cfg, nil 186 } 187 } 188 189 dftCfg := defaultConfig() 190 return &dftCfg, nil 191 } 192 193 func readConfByName(name string) (*Config, error) { 194 var path string 195 if wd := os.Getenv(airWd); wd != "" { 196 path = filepath.Join(wd, name) 197 } else { 198 wd, err := os.Getwd() 199 if err != nil { 200 return nil, err 201 } 202 path = filepath.Join(wd, name) 203 } 204 cfg, err := readConfig(path) 205 return cfg, err 206 } 207 208 func defaultConfig() Config { 209 build := cfgBuild{ 210 Cmd: "go build -o ./tmp/main .", 211 Bin: "./tmp/main", 212 Log: "build-errors.log", 213 IncludeExt: []string{"go", "tpl", "tmpl", "html"}, 214 IncludeDir: []string{}, 215 PreCmd: []string{}, 216 PostCmd: []string{}, 217 ExcludeFile: []string{}, 218 IncludeFile: []string{}, 219 ExcludeDir: []string{"assets", "tmp", "vendor", "testdata"}, 220 ArgsBin: []string{}, 221 ExcludeRegex: []string{"_test.go"}, 222 Delay: 1000, 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 err != nil { 293 return err 294 } 295 if c.TmpDir == "" { 296 c.TmpDir = "tmp" 297 } 298 if c.TestDataDir == "" { 299 c.TestDataDir = "testdata" 300 } 301 if err != nil { 302 return err 303 } 304 ed := c.Build.ExcludeDir 305 for i := range ed { 306 ed[i] = cleanPath(ed[i]) 307 } 308 309 adaptToVariousPlatforms(c) 310 311 // Join runtime arguments with the configuration arguments 312 runtimeArgs := flag.Args() 313 c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...) 314 315 c.Build.ExcludeDir = ed 316 if len(c.Build.FullBin) > 0 { 317 c.Build.Bin = c.Build.FullBin 318 return err 319 } 320 // Fix windows CMD processor 321 // CMD will not recognize relative path like ./tmp/server 322 c.Build.Bin, err = filepath.Abs(c.Build.Bin) 323 324 return err 325 } 326 327 func (c *Config) colorInfo() map[string]string { 328 return map[string]string{ 329 "main": c.Color.Main, 330 "build": c.Color.Build, 331 "runner": c.Color.Runner, 332 "watcher": c.Color.Watcher, 333 } 334 } 335 336 func (c *Config) buildLogPath() string { 337 return filepath.Join(c.tmpPath(), c.Build.Log) 338 } 339 340 func (c *Config) buildDelay() time.Duration { 341 return time.Duration(c.Build.Delay) * time.Millisecond 342 } 343 344 func (c *Config) rerunDelay() time.Duration { 345 return time.Duration(c.Build.RerunDelay) * time.Millisecond 346 } 347 348 func (c *Config) binPath() string { 349 return filepath.Join(c.Root, c.Build.Bin) 350 } 351 352 func (c *Config) tmpPath() string { 353 return filepath.Join(c.Root, c.TmpDir) 354 } 355 356 func (c *Config) testDataPath() string { 357 return filepath.Join(c.Root, c.TestDataDir) 358 } 359 360 func (c *Config) rel(path string) string { 361 s, err := filepath.Rel(c.Root, path) 362 if err != nil { 363 return "" 364 } 365 return s 366 } 367 368 // WithArgs returns a new config with the given arguments added to the configuration. 369 func (c *Config) WithArgs(args map[string]TomlInfo) { 370 for _, value := range args { 371 if value.Value != nil && *value.Value != unsetDefault { 372 v := reflect.ValueOf(c) 373 setValue2Struct(v, value.fieldPath, *value.Value) 374 } 375 } 376 }