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