github.com/zhb127/air@v0.0.2-0.20231109030911-fb911e430cdd/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  				c.Build.FullBin += extName
   245  			}
   246  			if !strings.HasPrefix(c.Build.FullBin, runName) {
   247  				c.Build.FullBin = runName + " /wait /b " + c.Build.FullBin
   248  			}
   249  		}
   250  
   251  		// bin=/tmp/main  cmd=go build -o ./tmp/main.exe main.go
   252  		if !strings.Contains(c.Build.Cmd, c.Build.Bin) && strings.Contains(c.Build.Cmd, originBin) {
   253  			c.Build.Cmd = strings.Replace(c.Build.Cmd, originBin, c.Build.Bin, 1)
   254  		}
   255  	}
   256  }
   257  
   258  // fileChecksum returns a checksum for the given file's contents.
   259  func fileChecksum(filename string) (checksum string, err error) {
   260  	contents, err := os.ReadFile(filename)
   261  	if err != nil {
   262  		return "", err
   263  	}
   264  
   265  	// If the file is empty, an editor might've been in the process of rewriting the file when we read it.
   266  	// This can happen often if editors are configured to run format after save.
   267  	// Instead of calculating a new checksum, we'll assume the file was unchanged, but return an error to force a rebuild anyway.
   268  	if len(contents) == 0 {
   269  		return "", errors.New("empty file, forcing rebuild without updating checksum")
   270  	}
   271  
   272  	h := sha256.New()
   273  	if _, err := h.Write(contents); err != nil {
   274  		return "", err
   275  	}
   276  
   277  	return hex.EncodeToString(h.Sum(nil)), nil
   278  }
   279  
   280  // checksumMap is a thread-safe map to store file checksums.
   281  type checksumMap struct {
   282  	l sync.Mutex
   283  	m map[string]string
   284  }
   285  
   286  // updateFileChecksum updates the filename with the given checksum if different.
   287  func (a *checksumMap) updateFileChecksum(filename, newChecksum string) (ok bool) {
   288  	a.l.Lock()
   289  	defer a.l.Unlock()
   290  	oldChecksum, ok := a.m[filename]
   291  	if !ok || oldChecksum != newChecksum {
   292  		a.m[filename] = newChecksum
   293  		return true
   294  	}
   295  	return false
   296  }
   297  
   298  // TomlInfo is a struct for toml config file
   299  type TomlInfo struct {
   300  	fieldPath string
   301  	field     reflect.StructField
   302  	Value     *string
   303  }
   304  
   305  func setValue2Struct(v reflect.Value, fieldName string, value string) {
   306  	index := strings.Index(fieldName, ".")
   307  	if index == -1 && len(fieldName) == 0 {
   308  		return
   309  	}
   310  	fields := strings.Split(fieldName, ".")
   311  	var addressableVal reflect.Value
   312  	switch v.Type().String() {
   313  	case "*runner.Config":
   314  		addressableVal = v.Elem()
   315  	default:
   316  		addressableVal = v
   317  	}
   318  	if len(fields) == 1 {
   319  		// string slice int switch case
   320  		field := addressableVal.FieldByName(fieldName)
   321  		switch field.Kind() {
   322  		case reflect.String:
   323  			field.SetString(value)
   324  		case reflect.Slice:
   325  			if len(value) == 0 {
   326  				field.Set(reflect.ValueOf([]string{}))
   327  			} else {
   328  				field.Set(reflect.ValueOf(strings.Split(value, sliceCmdArgSeparator)))
   329  			}
   330  		case reflect.Int64:
   331  			i, _ := strconv.ParseInt(value, 10, 64)
   332  			field.SetInt(i)
   333  		case reflect.Int:
   334  			i, _ := strconv.Atoi(value)
   335  			field.SetInt(int64(i))
   336  		case reflect.Bool:
   337  			b, _ := strconv.ParseBool(value)
   338  			field.SetBool(b)
   339  		case reflect.Ptr:
   340  			field.SetString(value)
   341  		default:
   342  			log.Fatalf("unsupported type %s", v.FieldByName(fields[0]).Kind())
   343  		}
   344  	} else if len(fields) == 0 {
   345  		return
   346  	} else {
   347  		field := addressableVal.FieldByName(fields[0])
   348  		s2 := fieldName[index+1:]
   349  		setValue2Struct(field, s2, value)
   350  	}
   351  }
   352  
   353  // flatConfig ...
   354  func flatConfig(stut interface{}) map[string]TomlInfo {
   355  	m := make(map[string]TomlInfo)
   356  	t := reflect.TypeOf(stut)
   357  	setTage2Map("", t, m, "")
   358  	return m
   359  }
   360  
   361  func setTage2Map(root string, t reflect.Type, m map[string]TomlInfo, fieldPath string) {
   362  	for i := 0; i < t.NumField(); i++ {
   363  		field := t.Field(i)
   364  		tomlVal := field.Tag.Get("toml")
   365  		switch field.Type.Kind() {
   366  		case reflect.Struct:
   367  			path := fieldPath + field.Name + "."
   368  			setTage2Map(root+tomlVal+".", field.Type, m, path)
   369  		default:
   370  			if tomlVal == "" {
   371  				continue
   372  			}
   373  			tomlPath := root + tomlVal
   374  			path := fieldPath + field.Name
   375  			var v *string
   376  			str := ""
   377  			v = &str
   378  			m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path}
   379  		}
   380  	}
   381  }