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