github.com/connorvict/air@v0.0.0-20231005162537-279bf07db0d5/runner/util.go (about) 1 package runner 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "errors" 7 "log" 8 "os" 9 "path/filepath" 10 "reflect" 11 "runtime" 12 "strconv" 13 "strings" 14 "sync" 15 16 "github.com/fsnotify/fsnotify" 17 ) 18 19 const ( 20 sliceCmdArgSeparator = "," 21 ) 22 23 func (e *Engine) mainLog(format string, v ...interface{}) { 24 e.logWithLock(func() { 25 e.logger.main()(format, v...) 26 }) 27 } 28 29 func (e *Engine) mainDebug(format string, v ...interface{}) { 30 if e.debugMode { 31 e.mainLog(format, v...) 32 } 33 } 34 35 func (e *Engine) buildLog(format string, v ...interface{}) { 36 if e.debugMode || !e.config.Log.MainOnly { 37 e.logWithLock(func() { 38 e.logger.build()(format, v...) 39 }) 40 } 41 } 42 43 func (e *Engine) runnerLog(format string, v ...interface{}) { 44 if e.debugMode || !e.config.Log.MainOnly { 45 e.logWithLock(func() { 46 e.logger.runner()(format, v...) 47 }) 48 } 49 } 50 51 func (e *Engine) watcherLog(format string, v ...interface{}) { 52 if e.debugMode || !e.config.Log.MainOnly { 53 e.logWithLock(func() { 54 e.logger.watcher()(format, v...) 55 }) 56 } 57 } 58 59 func (e *Engine) watcherDebug(format string, v ...interface{}) { 60 if e.debugMode { 61 e.watcherLog(format, v...) 62 } 63 } 64 65 func (e *Engine) isTmpDir(path string) bool { 66 return path == e.config.tmpPath() 67 } 68 69 func (e *Engine) isTestDataDir(path string) bool { 70 return path == e.config.testDataPath() 71 } 72 73 func isHiddenDirectory(path string) bool { 74 return len(path) > 1 && strings.HasPrefix(filepath.Base(path), ".") && filepath.Base(path) != ".." 75 } 76 77 func cleanPath(path string) string { 78 return strings.TrimSuffix(strings.TrimSpace(path), "/") 79 } 80 81 func (e *Engine) isExcludeDir(path string) bool { 82 cleanName := cleanPath(e.config.rel(path)) 83 for _, d := range e.config.Build.ExcludeDir { 84 if cleanName == d { 85 return true 86 } 87 } 88 return false 89 } 90 91 // return isIncludeDir, walkDir 92 func (e *Engine) checkIncludeDir(path string) (bool, bool) { 93 cleanName := cleanPath(e.config.rel(path)) 94 iDir := e.config.Build.IncludeDir 95 if len(iDir) == 0 { // ignore empty 96 return true, true 97 } 98 if cleanName == "." { 99 return false, true 100 } 101 walkDir := false 102 for _, d := range iDir { 103 if d == cleanName { 104 return true, true 105 } 106 if strings.HasPrefix(cleanName, d) { // current dir is sub-directory of `d` 107 return true, true 108 } 109 if strings.HasPrefix(d, cleanName) { // `d` is sub-directory of current dir 110 walkDir = true 111 } 112 } 113 return false, walkDir 114 } 115 116 func (e *Engine) checkIncludeFile(path string) bool { 117 cleanName := cleanPath(e.config.rel(path)) 118 iFile := e.config.Build.IncludeFile 119 if len(iFile) == 0 { // ignore empty 120 return false 121 } 122 if cleanName == "." { 123 return false 124 } 125 for _, d := range iFile { 126 if d == cleanName { 127 return true 128 } 129 } 130 return false 131 } 132 133 func (e *Engine) isIncludeExt(path string) bool { 134 ext := filepath.Ext(path) 135 for _, v := range e.config.Build.IncludeExt { 136 if ext == "."+strings.TrimSpace(v) { 137 return true 138 } 139 } 140 return false 141 } 142 143 func (e *Engine) isExcludeRegex(path string) (bool, error) { 144 regexes, err := e.config.Build.RegexCompiled() 145 if err != nil { 146 return false, err 147 } 148 for _, re := range regexes { 149 if re.Match([]byte(path)) { 150 return true, nil 151 } 152 } 153 return false, nil 154 } 155 156 func (e *Engine) isExcludeFile(path string) bool { 157 cleanName := cleanPath(e.config.rel(path)) 158 for _, d := range e.config.Build.ExcludeFile { 159 matched, err := filepath.Match(d, cleanName) 160 if err == nil && matched { 161 return true 162 } 163 } 164 return false 165 } 166 167 func (e *Engine) writeBuildErrorLog(msg string) error { 168 var err error 169 f, err := os.OpenFile(e.config.buildLogPath(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 170 if err != nil { 171 return err 172 } 173 if _, err = f.Write([]byte(msg)); err != nil { 174 return err 175 } 176 return f.Close() 177 } 178 179 func (e *Engine) withLock(f func()) { 180 e.mu.Lock() 181 f() 182 e.mu.Unlock() 183 } 184 185 func (e *Engine) logWithLock(f func()) { 186 e.ll.Lock() 187 f() 188 e.ll.Unlock() 189 } 190 191 func expandPath(path string) (string, error) { 192 if strings.HasPrefix(path, "~/") { 193 home := os.Getenv("HOME") 194 return home + path[1:], nil 195 } 196 var err error 197 wd, err := os.Getwd() 198 if err != nil { 199 return "", err 200 } 201 if path == "." { 202 return wd, nil 203 } 204 if strings.HasPrefix(path, "./") { 205 return wd + path[1:], nil 206 } 207 return path, nil 208 } 209 210 func isDir(path string) bool { 211 i, err := os.Stat(path) 212 if err != nil { 213 return false 214 } 215 return i.IsDir() 216 } 217 218 func validEvent(ev fsnotify.Event) bool { 219 return ev.Op&fsnotify.Create == fsnotify.Create || 220 ev.Op&fsnotify.Write == fsnotify.Write || 221 ev.Op&fsnotify.Remove == fsnotify.Remove 222 } 223 224 func removeEvent(ev fsnotify.Event) bool { 225 return ev.Op&fsnotify.Remove == fsnotify.Remove 226 } 227 228 func cmdPath(path string) string { 229 return strings.Split(path, " ")[0] 230 } 231 232 func adaptToVariousPlatforms(c *Config) { 233 // Fix the default configuration is not used in Windows 234 // Use the unix configuration on Windows 235 if runtime.GOOS == PlatformWindows { 236 237 runName := "start" 238 extName := ".exe" 239 originBin := c.Build.Bin 240 241 if 0 < len(c.Build.FullBin) { 242 243 if !strings.HasSuffix(c.Build.FullBin, extName) { 244 245 c.Build.FullBin += extName 246 } 247 if !strings.HasPrefix(c.Build.FullBin, runName) { 248 c.Build.FullBin = runName + " /wait /b " + c.Build.FullBin 249 } 250 } 251 252 // bin=/tmp/main cmd=go build -o ./tmp/main.exe main.go 253 if !strings.Contains(c.Build.Cmd, c.Build.Bin) && strings.Contains(c.Build.Cmd, originBin) { 254 c.Build.Cmd = strings.Replace(c.Build.Cmd, originBin, c.Build.Bin, 1) 255 } 256 } 257 } 258 259 // fileChecksum returns a checksum for the given file's contents. 260 func fileChecksum(filename string) (checksum string, err error) { 261 contents, err := os.ReadFile(filename) 262 if err != nil { 263 return "", err 264 } 265 266 // If the file is empty, an editor might've been in the process of rewriting the file when we read it. 267 // This can happen often if editors are configured to run format after save. 268 // Instead of calculating a new checksum, we'll assume the file was unchanged, but return an error to force a rebuild anyway. 269 if len(contents) == 0 { 270 return "", errors.New("empty file, forcing rebuild without updating checksum") 271 } 272 273 h := sha256.New() 274 if _, err := h.Write(contents); err != nil { 275 return "", err 276 } 277 278 return hex.EncodeToString(h.Sum(nil)), nil 279 } 280 281 // checksumMap is a thread-safe map to store file checksums. 282 type checksumMap struct { 283 l sync.Mutex 284 m map[string]string 285 } 286 287 // updateFileChecksum updates the filename with the given checksum if different. 288 func (a *checksumMap) updateFileChecksum(filename, newChecksum string) (ok bool) { 289 a.l.Lock() 290 defer a.l.Unlock() 291 oldChecksum, ok := a.m[filename] 292 if !ok || oldChecksum != newChecksum { 293 a.m[filename] = newChecksum 294 return true 295 } 296 return false 297 } 298 299 // TomlInfo is a struct for toml config file 300 type TomlInfo struct { 301 fieldPath string 302 field reflect.StructField 303 Value *string 304 } 305 306 func setValue2Struct(v reflect.Value, fieldName string, value string) { 307 index := strings.Index(fieldName, ".") 308 if index == -1 && len(fieldName) == 0 { 309 return 310 } 311 fields := strings.Split(fieldName, ".") 312 var addressableVal reflect.Value 313 switch v.Type().String() { 314 case "*runner.Config": 315 addressableVal = v.Elem() 316 default: 317 addressableVal = v 318 } 319 if len(fields) == 1 { 320 // string slice int switch case 321 field := addressableVal.FieldByName(fieldName) 322 switch field.Kind() { 323 case reflect.String: 324 field.SetString(value) 325 case reflect.Slice: 326 if len(value) == 0 { 327 field.Set(reflect.ValueOf([]string{})) 328 } else { 329 field.Set(reflect.ValueOf(strings.Split(value, sliceCmdArgSeparator))) 330 } 331 case reflect.Int64: 332 i, _ := strconv.ParseInt(value, 10, 64) 333 field.SetInt(i) 334 case reflect.Int: 335 i, _ := strconv.Atoi(value) 336 field.SetInt(int64(i)) 337 case reflect.Bool: 338 b, _ := strconv.ParseBool(value) 339 field.SetBool(b) 340 case reflect.Ptr: 341 field.SetString(value) 342 default: 343 log.Fatalf("unsupported type %s", v.FieldByName(fields[0]).Kind()) 344 } 345 } else if len(fields) == 0 { 346 return 347 } else { 348 field := addressableVal.FieldByName(fields[0]) 349 s2 := fieldName[index+1:] 350 setValue2Struct(field, s2, value) 351 } 352 } 353 354 // flatConfig ... 355 func flatConfig(stut interface{}) map[string]TomlInfo { 356 m := make(map[string]TomlInfo) 357 t := reflect.TypeOf(stut) 358 setTage2Map("", t, m, "") 359 return m 360 } 361 362 func setTage2Map(root string, t reflect.Type, m map[string]TomlInfo, fieldPath string) { 363 for i := 0; i < t.NumField(); i++ { 364 field := t.Field(i) 365 tomlVal := field.Tag.Get("toml") 366 switch field.Type.Kind() { 367 case reflect.Struct: 368 path := fieldPath + field.Name + "." 369 setTage2Map(root+tomlVal+".", field.Type, m, path) 370 default: 371 if tomlVal == "" { 372 continue 373 } 374 tomlPath := root + tomlVal 375 path := fieldPath + field.Name 376 var v *string 377 str := "" 378 v = &str 379 m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path} 380 } 381 } 382 }