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