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