Files
air/runner/config.go
T
Marius van Niekerk 547626a83c fix: keep built binary after app shutdown (#911)
Air could delete build.entrypoint while stopping the previous app process when stop_on_error was enabled. After builds were changed to keep the old app alive until a replacement build succeeds, that cleanup could remove the freshly rebuilt binary before it was started, causing issue #910.

Failed builds should not delete the last executable either. Keeping the binary in place makes stop_on_error stop the process without destroying the artifact a later rebuild or manual restart may need.

Windows needs different sequencing because running executables are locked. Stop the old process before build.cmd on Windows, while keeping the retained-app behavior on Unix platforms.

Validation: go test ./runner -run 'TestAddPlatformOverridesForInit|TestPlatformBuildOverridesSelection|TestShouldStopBinBeforeBuild|TestStopBinBeforeBuildIfNeeded|TestBuildRunKeepsIssue910GoBuildEntrypoint' -count=1 -v; go test ./runner -run 'TestBuildRunKeepsIssue910GoBuildEntrypoint|TestShouldStopBinBeforeBuild|TestBuildRunKeeps.*Binary.*StopOnError|TestBuildRunStopsExistingBinWhenBuildFailsWithStopOnError|TestBuildRunStopsExistingBinAfterSuccessfulBuild|TestRebuild$' -count=1 -v; go test ./...; make check

🤖 Generated with [OpenAI Codex](https://openai.com/codex)

Co-authored-by: Marius van Niekerk <mariusvniekerk@mbp-marius-kenn.emperor-gopher.ts.net>
Co-authored-by: OpenAI Codex <noreply@openai.com>
2026-05-21 22:50:40 +08:00

842 lines
22 KiB
Go

package runner
import (
"errors"
"flag"
"fmt"
"os"
"os/exec"
pathpkg "path"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"time"
"dario.cat/mergo"
"github.com/fatih/color"
"github.com/pelletier/go-toml"
)
const (
cfgTOML = ".config/air.toml"
dftTOML = ".air.toml"
airWd = "air_wd"
defaultProxyAppStartTimeout = 5000
schemaHeader = "#:schema https://json.schemastore.org/any.json"
)
// Config is the main configuration structure for Air.
type Config struct {
Root string `toml:"root" usage:"Working directory, . or absolute path, please note that the directories following must be under root"`
TmpDir string `toml:"tmp_dir" usage:"Temporary directory for air"`
TestDataDir string `toml:"testdata_dir"`
EnvFiles []string `toml:"env_files" usage:"Paths to .env files to load before build/run"`
Build cfgBuild `toml:"build"`
Color cfgColor `toml:"color"`
Log cfgLog `toml:"log"`
Misc cfgMisc `toml:"misc"`
Screen cfgScreen `toml:"screen"`
Proxy cfgProxy `toml:"proxy"`
}
type entrypoint []string
func (e *entrypoint) UnmarshalTOML(v interface{}) error {
switch val := v.(type) {
case nil:
*e = nil
return nil
case string:
*e = []string{val}
return nil
case []interface{}:
values := make([]string, len(val))
for i, raw := range val {
s, ok := raw.(string)
if !ok {
return fmt.Errorf("entrypoint values must be strings, got %T", raw)
}
values[i] = s
}
*e = values
return nil
default:
return fmt.Errorf("entrypoint must be a string or array of strings, got %T", v)
}
}
func (e entrypoint) binary() string {
if len(e) == 0 {
return ""
}
return e[0]
}
func (e entrypoint) args() []string {
if len(e) <= 1 {
return nil
}
return e[1:]
}
type cfgBuildOverrides struct {
CfgBuildCommon `toml:",inline"`
}
type CfgBuildCommon struct {
PreCmd []string `toml:"pre_cmd" usage:"Array of commands to run before each build"`
Cmd string `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"`
PostCmd []string `toml:"post_cmd" usage:"Array of commands to run after ^C"`
Bin string `toml:"bin" usage:"Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint."`
Entrypoint entrypoint `toml:"entrypoint" usage:"Binary file plus optional arguments relative to root, prefer [\"./tmp/main\", \"arg\"] form"`
FullBin string `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"`
ArgsBin []string `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."`
}
type cfgBuild struct {
CfgBuildCommon `toml:",inline"`
Log string `toml:"log" usage:"This log file is placed in your tmp_dir"`
IncludeExt []string `toml:"include_ext" usage:"Watch these filename extensions"`
ExcludeDir []string `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"`
IncludeDir []string `toml:"include_dir" usage:"Watch these directories if you specified"`
ExcludeFile []string `toml:"exclude_file" usage:"Exclude files"`
IncludeFile []string `toml:"include_file" usage:"Watch these files"`
ExcludeRegex []string `toml:"exclude_regex" usage:"Exclude specific regular expressions"`
ExcludeUnchanged bool `toml:"exclude_unchanged" usage:"Exclude unchanged files"`
IgnoreDangerousRootDir bool `toml:"ignore_dangerous_root_dir" usage:"Ignore dangerous root directory that could cause excessive file watching"`
FollowSymlink bool `toml:"follow_symlink" usage:"Follow symlink for directories"`
Poll bool `toml:"poll" usage:"Poll files for changes instead of using fsnotify"`
PollInterval int `toml:"poll_interval" usage:"Poll interval (defaults to the minimum interval of 500ms)"`
Delay int `toml:"delay" usage:"It's not necessary to trigger build each time file changes if it's too frequent"`
StopOnError bool `toml:"stop_on_error" usage:"Stop running old binary when build errors occur"`
SendInterrupt bool `toml:"send_interrupt" usage:"Send Interrupt signal before killing process (windows does not support this feature)"`
KillDelay time.Duration `toml:"kill_delay" usage:"Delay after sending Interrupt signal"`
Rerun bool `toml:"rerun" usage:"Rerun binary or not"`
RerunDelay int `toml:"rerun_delay" usage:"Delay after each execution"`
Windows *cfgBuildOverrides `toml:"windows,omitempty"`
Darwin *cfgBuildOverrides `toml:"darwin,omitempty"`
Linux *cfgBuildOverrides `toml:"linux,omitempty"`
regexCompiled []*regexp.Regexp
includeDirAbs []string
extraIncludeDirs []string
}
func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) {
return c.regexCompiled, nil
}
func (c *cfgBuild) normalizeIncludeDirs(root string) {
c.includeDirAbs = c.includeDirAbs[:0]
c.extraIncludeDirs = c.extraIncludeDirs[:0]
if root == "" {
return
}
for _, dir := range c.IncludeDir {
dir = cleanPath(dir)
if dir == "" {
continue
}
dir = filepath.Clean(dir)
abs := dir
if !filepath.IsAbs(dir) {
abs = filepath.Join(root, dir)
}
abs = filepath.Clean(abs)
if isSubPath(root, abs) {
c.includeDirAbs = append(c.includeDirAbs, abs)
continue
}
c.extraIncludeDirs = append(c.extraIncludeDirs, abs)
}
}
type cfgLog struct {
AddTime bool `toml:"time" usage:"Show log time"`
MainOnly bool `toml:"main_only" usage:"Only show main log (silences watcher, build, runner)"`
Silent bool `toml:"silent" usage:"silence all logs produced by air"`
}
type cfgColor struct {
Main string `toml:"main" usage:"Customize main part's color. If no color found, use the raw app log"`
Watcher string `toml:"watcher" usage:"Customize watcher part's color"`
Build string `toml:"build" usage:"Customize build part's color"`
Runner string `toml:"runner" usage:"Customize runner part's color"`
Mode string `toml:"mode" usage:"Colorized output mode, one of always, auto, or never. Defaults to auto"`
App string `toml:"app"`
}
type cfgMisc struct {
CleanOnExit bool `toml:"clean_on_exit" usage:"Delete tmp directory on exit"`
StartupBanner *string `toml:"startup_banner" usage:"Custom startup banner text; set to empty string to hide banner"`
}
type cfgScreen struct {
ClearOnRebuild bool `toml:"clear_on_rebuild" usage:"Clear screen on rebuild"`
KeepScroll bool `toml:"keep_scroll" usage:"Keep scroll position after rebuild"`
}
type cfgProxy struct {
Enabled bool `toml:"enabled" usage:"Enable live-reloading on the browser"`
ProxyPort int `toml:"proxy_port" usage:"Port for proxy server"`
AppPort int `toml:"app_port" usage:"Port for your app"`
AppStartTimeout int `toml:"app_start_timeout" usage:"Timeout for waiting for app to start in milliseconds (default 5000)"`
}
type sliceTransformer struct{}
func (t sliceTransformer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error {
if typ.Kind() == reflect.Slice {
return func(dst, src reflect.Value) error {
if !src.IsZero() {
dst.Set(src)
}
return nil
}
}
return nil
}
// InitConfig initializes the configuration.
func InitConfig(path string, cmdArgs map[string]TomlInfo) (*Config, error) {
ret, err := initConfigWithoutPreprocess(path)
if err != nil {
return nil, err
}
err = ret.preprocess(cmdArgs)
if err != nil {
return nil, err
}
warnDeprecatedBin(ret)
return ret, nil
}
// InitConfigForDisplay initializes config without preprocess side effects.
func InitConfigForDisplay(path string, cmdArgs map[string]TomlInfo) (*Config, error) {
ret, err := initConfigWithoutPreprocess(path)
if err != nil {
return nil, err
}
if cmdArgs != nil {
ret.withArgs(cmdArgs)
}
return ret, nil
}
func initConfigWithoutPreprocess(path string) (*Config, error) {
var (
cfg *Config
err error
)
if path == "" {
cfg, err = defaultPathConfig()
if err != nil {
return nil, err
}
} else {
cfg, err = readConfigOrDefault(path)
if err != nil {
return nil, err
}
}
config := defaultConfig()
// get addr
ret := &config
err = mergo.Merge(ret, cfg, func(config *mergo.Config) {
// mergo.Merge will overwrite the fields if it is Empty
// So need use this to avoid that none-zero slice will be overwritten.
// https://dario.cat/mergo#transformers
config.Transformers = sliceTransformer{}
config.Overwrite = true
})
if err != nil {
return nil, err
}
if err = applyPlatformOverrides(ret); err != nil {
return nil, err
}
return ret, nil
}
func writeDefaultConfig() (string, error) {
fstat, err := os.Stat(dftTOML)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("failed to check for existing configuration: %w", err)
}
if err == nil && fstat != nil {
return "", errors.New("configuration already exists")
}
file, err := os.Create(dftTOML)
if err != nil {
return "", fmt.Errorf("failed to create a new configuration: %w", err)
}
defer file.Close()
config := defaultConfigBase()
setEntrypointFromBin(&config)
addPlatformOverridesForInit(&config, runtime.GOOS)
configFile, err := toml.Marshal(config)
if err != nil {
return "", fmt.Errorf("failed to marshal the default configuration: %w", err)
}
headers := []byte(schemaHeader + "\n\n")
content := append(headers, configFile...)
_, err = file.Write(content)
if err != nil {
return "", fmt.Errorf("failed to write to %s: %w", dftTOML, err)
}
return dftTOML, nil
}
func defaultPathConfig() (*Config, error) {
// when path is blank, first find `.air.toml` in `air_wd` and current working directory or .config/air.toml, if not found, use defaults
for _, name := range []string{dftTOML, cfgTOML} {
cfg, err := readConfByName(name)
if err == nil {
return cfg, nil
}
// If the config file exists but failed to parse, report the error
// Only use defaults if no config file exists
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to parse %s: %w", name, err)
}
}
dftCfg := defaultConfig()
setEntrypointFromBin(&dftCfg)
return &dftCfg, nil
}
func readConfByName(name string) (*Config, error) {
var path string
if wd := os.Getenv(airWd); wd != "" {
path = filepath.Join(wd, name)
} else {
wd, err := os.Getwd()
if err != nil {
return nil, err
}
path = filepath.Join(wd, name)
}
cfg, err := readConfig(path)
return cfg, err
}
func defaultConfigBase() Config {
build := cfgBuild{
CfgBuildCommon: CfgBuildCommon{
Cmd: "go build -o ./tmp/main .",
Bin: "./tmp/main",
Entrypoint: entrypoint{},
PreCmd: []string{},
PostCmd: []string{},
ArgsBin: []string{},
},
Log: "build-errors.log",
IncludeExt: []string{"go", "tpl", "tmpl", "html"},
IncludeDir: []string{},
ExcludeFile: []string{},
IncludeFile: []string{},
ExcludeDir: []string{"assets", "tmp", "vendor", "testdata"},
ExcludeRegex: []string{"_test.go"},
Delay: 1000,
Rerun: false,
RerunDelay: 500,
}
log := cfgLog{
AddTime: false,
MainOnly: false,
Silent: false,
}
color := cfgColor{
Main: "magenta",
Watcher: "cyan",
Build: "yellow",
Runner: "green",
}
misc := cfgMisc{
CleanOnExit: false,
}
return Config{
Root: ".",
TmpDir: "tmp",
TestDataDir: "testdata",
EnvFiles: []string{},
Build: build,
Color: color,
Log: log,
Misc: misc,
Screen: cfgScreen{
ClearOnRebuild: false,
KeepScroll: true,
},
}
}
func defaultConfigForOS(goos string) Config {
cfg := defaultConfigBase()
applyPlatformDefaults(&cfg, goos)
return cfg
}
func defaultConfig() Config {
return defaultConfigForOS(runtime.GOOS)
}
func applyPlatformDefaults(cfg *Config, goos string) {
if cfg == nil {
return
}
if goos == PlatformWindows {
cfg.Build.Bin = `tmp\main.exe`
cfg.Build.Cmd = "go build -o ./tmp/main.exe ."
}
}
func applyPlatformOverrides(cfg *Config) error {
if cfg == nil {
return nil
}
override := platformBuildOverrides(&cfg.Build, runtime.GOOS)
if override == nil {
return nil
}
applyBuildOverrides(&cfg.Build, override)
return nil
}
func platformBuildOverrides(build *cfgBuild, goos string) *cfgBuildOverrides {
if build == nil {
return nil
}
switch goos {
case PlatformWindows:
return build.Windows
case PlatformDarwin:
return build.Darwin
case PlatformLinux:
return build.Linux
default:
return nil
}
}
func applyBuildOverrides(build *cfgBuild, override *cfgBuildOverrides) {
if build == nil || override == nil {
return
}
if override.PreCmd != nil {
build.PreCmd = override.PreCmd
}
if override.Cmd != "" {
build.Cmd = override.Cmd
}
if override.PostCmd != nil {
build.PostCmd = override.PostCmd
}
if override.Bin != "" {
build.Bin = override.Bin
}
if override.Entrypoint != nil {
build.Entrypoint = override.Entrypoint
}
if override.FullBin != "" {
build.FullBin = override.FullBin
}
if override.ArgsBin != nil {
build.ArgsBin = override.ArgsBin
}
}
func setEntrypointFromBin(cfg *Config) {
if cfg == nil {
return
}
if len(cfg.Build.Entrypoint) == 0 && cfg.Build.Bin != "" {
cfg.Build.Entrypoint = entrypoint{cfg.Build.Bin}
}
}
func addPlatformOverridesForInit(cfg *Config, goos string) {
if cfg == nil {
return
}
platformConfig := defaultConfigForOS(goos)
setEntrypointFromBin(&platformConfig)
override := buildOverridesFromDiff(cfg.Build, platformConfig.Build)
if override == nil {
return
}
switch goos {
case PlatformWindows:
cfg.Build.Windows = override
case PlatformDarwin:
cfg.Build.Darwin = override
case PlatformLinux:
cfg.Build.Linux = override
}
}
func buildOverridesFromDiff(base cfgBuild, target cfgBuild) *cfgBuildOverrides {
var override cfgBuildOverrides
changed := false
if !reflect.DeepEqual(base.PreCmd, target.PreCmd) {
override.PreCmd = target.PreCmd
changed = true
}
if base.Cmd != target.Cmd {
override.Cmd = target.Cmd
changed = true
}
if !reflect.DeepEqual(base.PostCmd, target.PostCmd) {
override.PostCmd = target.PostCmd
changed = true
}
if base.Bin != target.Bin {
override.Bin = target.Bin
changed = true
}
if !reflect.DeepEqual(base.Entrypoint, target.Entrypoint) {
override.Entrypoint = target.Entrypoint
changed = true
}
if base.FullBin != target.FullBin {
override.FullBin = target.FullBin
changed = true
}
if !reflect.DeepEqual(base.ArgsBin, target.ArgsBin) {
override.ArgsBin = target.ArgsBin
changed = true
}
if !changed {
return nil
}
return &override
}
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
cfg := new(Config)
if err = toml.Unmarshal(data, cfg); err != nil {
return nil, err
}
return cfg, nil
}
func readConfigOrDefault(path string) (*Config, error) {
dftCfg := defaultConfig()
cfg, err := readConfig(path)
if err != nil {
return &dftCfg, err
}
return cfg, nil
}
func (c *Config) preprocess(args map[string]TomlInfo) error {
var err error
if args != nil {
c.withArgs(args)
}
cwd := os.Getenv(airWd)
if cwd != "" {
if err = os.Chdir(cwd); err != nil {
return err
}
c.Root = cwd
}
c.Root, err = expandPath(c.Root)
if err != nil {
return err
}
// Check for dangerous root directories that could cause excessive file watching
if isDangerous, dirName := isDangerousRoot(c.Root); isDangerous {
if !c.Build.IgnoreDangerousRootDir {
return fmt.Errorf("refusing to run in %s - this would watch too many files. Please run air in a project directory", dirName)
}
fmt.Fprintln(os.Stderr, "[warning] ignoring root directory protections. This could cause excessive file watching. It is recommended to run air in a project directory")
}
if c.TmpDir == "" {
c.TmpDir = "tmp"
}
if c.TestDataDir == "" {
c.TestDataDir = "testdata"
}
c.adjustDefaultsForTmpDir()
ed := c.Build.ExcludeDir
for i := range ed {
ed[i] = cleanPath(ed[i])
}
if len(c.Build.Entrypoint) > 0 {
entry := c.Build.Entrypoint.binary()
if !filepath.IsAbs(entry) {
if resolved := resolveCommandPath(entry); resolved != "" {
entry = resolved
} else {
entry = joinPath(c.Root, entry)
}
}
entry, err = filepath.Abs(entry)
if err != nil {
return err
}
c.Build.Entrypoint[0] = entry
}
adaptToVariousPlatforms(c)
c.Build.normalizeIncludeDirs(c.Root)
// Join runtime arguments with the configuration arguments
runtimeArgs := flag.Args()
c.Build.ArgsBin = append(c.Build.ArgsBin, runtimeArgs...)
// Compile the exclude regexes if there are any patterns in the config file
if len(c.Build.ExcludeRegex) > 0 {
regexCompiled := make([]*regexp.Regexp, len(c.Build.ExcludeRegex))
for idx, expr := range c.Build.ExcludeRegex {
re, err := regexp.Compile(expr)
if err != nil {
return fmt.Errorf("failed to compile regex %q: %w", expr, err)
}
regexCompiled[idx] = re
}
c.Build.regexCompiled = regexCompiled
}
c.Build.ExcludeDir = ed
// Set colorful output, see https://github.com/fatih/color#disableenable-color
switch c.Color.Mode {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto", "":
break
default:
return fmt.Errorf("unsupported color mode: %s. Expected always, auto, or never", c.Color.Mode)
}
if len(c.Build.FullBin) > 0 {
c.Build.Bin = c.Build.FullBin
return err
}
// Fix windows CMD processor
// CMD will not recognize relative path like ./tmp/server
c.Build.Bin, err = filepath.Abs(c.Build.Bin)
return err
}
// adjustDefaultsForTmpDir updates Build.Cmd, Build.Bin, and Build.ExcludeDir
// when they still hold their default values but TmpDir has been changed.
func (c *Config) adjustDefaultsForTmpDir() {
c.adjustDefaultsForTmpDirWithOS(runtime.GOOS)
}
func (c *Config) adjustDefaultsForTmpDirWithOS(goos string) {
const defaultTmpDir = "tmp"
if c.TmpDir == defaultTmpDir {
return
}
defaultCmd := "go build -o ./tmp/main ."
defaultBin := "./tmp/main"
mainBinary := "main"
if goos == PlatformWindows {
defaultCmd = "go build -o ./tmp/main.exe ."
defaultBin = `tmp\main.exe`
mainBinary = "main.exe"
}
newBinPath := filepath.Join(c.TmpDir, mainBinary)
normalizedBinPath := strings.ReplaceAll(newBinPath, `\`, "/")
newBin := "./" + normalizedBinPath
cmdOut := newBin
if isAbsPathForOS(goos, c.TmpDir) {
newBin = newBinPath
cmdOut = normalizedBinPath
}
if goos == PlatformWindows {
if isAbsPathForOS(goos, c.TmpDir) {
newBin = strings.ReplaceAll(newBinPath, "/", "\\")
cmdOut = normalizedBinPath
} else {
newBin = strings.ReplaceAll(newBinPath, "/", "\\")
cmdOut = "./" + normalizedBinPath
}
}
newCmd := "go build -o " + cmdOut + " ."
if c.Build.Cmd == defaultCmd {
c.Build.Cmd = newCmd
}
if c.Build.Bin == defaultBin {
c.Build.Bin = newBin
}
if isDefaultExcludeDir(c.Build.ExcludeDir) {
for i, dir := range c.Build.ExcludeDir {
if dir == defaultTmpDir {
c.Build.ExcludeDir[i] = c.TmpDir
}
}
}
}
func isDefaultExcludeDir(dirs []string) bool {
defaultDirs := []string{"assets", "tmp", "vendor", "testdata"}
if len(dirs) != len(defaultDirs) {
return false
}
for i := range dirs {
if dirs[i] != defaultDirs[i] {
return false
}
}
return true
}
func isAbsPathForOS(goos, path string) bool {
if goos != PlatformWindows {
return pathpkg.IsAbs(path)
}
if strings.HasPrefix(path, `\\`) {
return true
}
if len(path) < 3 {
return false
}
drive := path[0]
if ((drive >= 'a' && drive <= 'z') || (drive >= 'A' && drive <= 'Z')) && path[1] == ':' {
return path[2] == '\\' || path[2] == '/'
}
return false
}
func (c *Config) colorInfo() map[string]string {
return map[string]string{
"main": c.Color.Main,
"build": c.Color.Build,
"runner": c.Color.Runner,
"watcher": c.Color.Watcher,
}
}
func (c *Config) buildLogPath() string {
return joinPath(c.tmpPath(), c.Build.Log)
}
func (c *Config) buildDelay() time.Duration {
return time.Duration(c.Build.Delay) * time.Millisecond
}
func (c *Config) rerunDelay() time.Duration {
return time.Duration(c.Build.RerunDelay) * time.Millisecond
}
func (c *Config) killDelay() time.Duration {
// kill_delay can be specified as an integer or duration string
// interpret as milliseconds if less than the value of 1 millisecond
if c.Build.KillDelay < time.Millisecond {
return c.Build.KillDelay * time.Millisecond
}
// normalize kill delay to milliseconds
return time.Duration(c.Build.KillDelay.Milliseconds()) * time.Millisecond
}
func (c *Config) binPath() string {
if len(c.Build.Entrypoint) > 0 {
return c.Build.Entrypoint.binary()
}
return joinPath(c.Root, c.Build.Bin)
}
func (c *Config) runnerBin() string {
if len(c.Build.Entrypoint) > 0 && len(c.Build.FullBin) == 0 {
return c.Build.Entrypoint.binary()
}
return c.Build.Bin
}
func (c *Config) tmpPath() string {
return joinPath(c.Root, c.TmpDir)
}
func (c *Config) testDataPath() string {
return joinPath(c.Root, c.TestDataDir)
}
func (c *Config) rel(path string) string {
s, err := filepath.Rel(c.Root, path)
if err != nil {
return ""
}
return s
}
func resolveCommandPath(entry string) string {
if entry == "" || strings.ContainsAny(entry, `/\`) {
return ""
}
path, err := exec.LookPath(entry)
if err != nil {
return ""
}
return path
}
// withArgs returns a new config with the given arguments added to the configuration.
func (c *Config) withArgs(args map[string]TomlInfo) {
for _, value := range args {
// Ignore values that match the default configuration.
// This ensures user-specified configurations are not overwritten by default values.
if value.Value != nil && *value.Value != value.fieldValue {
v := reflect.ValueOf(c)
setValue2Struct(v, value.fieldPath, *value.Value)
}
}
}
func warnDeprecatedBin(cfg *Config) {
if cfg == nil {
return
}
if cfg.Build.Bin == "" || len(cfg.Build.Entrypoint) > 0 {
return
}
if cfg.Build.Bin != "" && len(cfg.Build.Entrypoint) > 0 {
return
}
fmt.Fprintln(os.Stderr, "[warning] build.bin is deprecated; set build.entrypoint instead")
}