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