github.com/zhb127/air@v0.0.2-0.20231109030911-fb911e430cdd/runner/config.go (about)

     1  package runner
     2  
     3  import (
     4  	"errors"
     5  	"flag"
     6  	"fmt"
     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() (string, error) {
   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  			return "", fmt.Errorf("failed to check for existing configuration: %w", err)
   151  		}
   152  		if err == nil && fstat != nil {
   153  			return "", errors.New("configuration already exists")
   154  		}
   155  	}
   156  
   157  	file, err := os.Create(dftTOML)
   158  	if err != nil {
   159  		return "", fmt.Errorf("failed to create a new configuration: %w", err)
   160  	}
   161  	defer file.Close()
   162  
   163  	config := defaultConfig()
   164  	configFile, err := toml.Marshal(config)
   165  	if err != nil {
   166  		return "", fmt.Errorf("failed to marshal the default configuration: %w", err)
   167  	}
   168  
   169  	_, err = file.Write(configFile)
   170  	if err != nil {
   171  		return "", fmt.Errorf("failed to write to %s: %w", dftTOML, err)
   172  	}
   173  
   174  	return dftTOML, nil
   175  }
   176  
   177  func defaultPathConfig() (*Config, error) {
   178  	// when path is blank, first find `.air.toml`, `.air.conf` in `air_wd` and current working directory, if not found, use defaults
   179  	for _, name := range []string{dftTOML, dftConf} {
   180  		cfg, err := readConfByName(name)
   181  		if err == nil {
   182  			if name == dftConf {
   183  				fmt.Println("`.air.conf` will be deprecated soon, recommend using `.air.toml`.")
   184  			}
   185  			return cfg, nil
   186  		}
   187  	}
   188  
   189  	dftCfg := defaultConfig()
   190  	return &dftCfg, nil
   191  }
   192  
   193  func readConfByName(name string) (*Config, error) {
   194  	var path string
   195  	if wd := os.Getenv(airWd); wd != "" {
   196  		path = filepath.Join(wd, name)
   197  	} else {
   198  		wd, err := os.Getwd()
   199  		if err != nil {
   200  			return nil, err
   201  		}
   202  		path = filepath.Join(wd, name)
   203  	}
   204  	cfg, err := readConfig(path)
   205  	return cfg, err
   206  }
   207  
   208  func defaultConfig() Config {
   209  	build := cfgBuild{
   210  		Cmd:          "go build -o ./tmp/main .",
   211  		Bin:          "./tmp/main",
   212  		Log:          "build-errors.log",
   213  		IncludeExt:   []string{"go", "tpl", "tmpl", "html"},
   214  		IncludeDir:   []string{},
   215  		PreCmd:       []string{},
   216  		PostCmd:      []string{},
   217  		ExcludeFile:  []string{},
   218  		IncludeFile:  []string{},
   219  		ExcludeDir:   []string{"assets", "tmp", "vendor", "testdata"},
   220  		ArgsBin:      []string{},
   221  		ExcludeRegex: []string{"_test.go"},
   222  		Delay:        1000,
   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 err != nil {
   293  		return err
   294  	}
   295  	if c.TmpDir == "" {
   296  		c.TmpDir = "tmp"
   297  	}
   298  	if c.TestDataDir == "" {
   299  		c.TestDataDir = "testdata"
   300  	}
   301  	if err != nil {
   302  		return err
   303  	}
   304  	ed := c.Build.ExcludeDir
   305  	for i := range ed {
   306  		ed[i] = cleanPath(ed[i])
   307  	}
   308  
   309  	adaptToVariousPlatforms(c)
   310  
   311  	// Join runtime arguments with the configuration arguments
   312  	runtimeArgs := flag.Args()
   313  	c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...)
   314  
   315  	c.Build.ExcludeDir = ed
   316  	if len(c.Build.FullBin) > 0 {
   317  		c.Build.Bin = c.Build.FullBin
   318  		return err
   319  	}
   320  	// Fix windows CMD processor
   321  	// CMD will not recognize relative path like ./tmp/server
   322  	c.Build.Bin, err = filepath.Abs(c.Build.Bin)
   323  
   324  	return err
   325  }
   326  
   327  func (c *Config) colorInfo() map[string]string {
   328  	return map[string]string{
   329  		"main":    c.Color.Main,
   330  		"build":   c.Color.Build,
   331  		"runner":  c.Color.Runner,
   332  		"watcher": c.Color.Watcher,
   333  	}
   334  }
   335  
   336  func (c *Config) buildLogPath() string {
   337  	return filepath.Join(c.tmpPath(), c.Build.Log)
   338  }
   339  
   340  func (c *Config) buildDelay() time.Duration {
   341  	return time.Duration(c.Build.Delay) * time.Millisecond
   342  }
   343  
   344  func (c *Config) rerunDelay() time.Duration {
   345  	return time.Duration(c.Build.RerunDelay) * time.Millisecond
   346  }
   347  
   348  func (c *Config) binPath() string {
   349  	return filepath.Join(c.Root, c.Build.Bin)
   350  }
   351  
   352  func (c *Config) tmpPath() string {
   353  	return filepath.Join(c.Root, c.TmpDir)
   354  }
   355  
   356  func (c *Config) testDataPath() string {
   357  	return filepath.Join(c.Root, c.TestDataDir)
   358  }
   359  
   360  func (c *Config) rel(path string) string {
   361  	s, err := filepath.Rel(c.Root, path)
   362  	if err != nil {
   363  		return ""
   364  	}
   365  	return s
   366  }
   367  
   368  // WithArgs returns a new config with the given arguments added to the configuration.
   369  func (c *Config) WithArgs(args map[string]TomlInfo) {
   370  	for _, value := range args {
   371  		if value.Value != nil && *value.Value != unsetDefault {
   372  			v := reflect.ValueOf(c)
   373  			setValue2Struct(v, value.fieldPath, *value.Value)
   374  		}
   375  	}
   376  }