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