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