github.com/yang-ricky/air@v1.30.0/runner/config.go (about)

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