package main import ( "errors" "fmt" "os" "path" "path/filepath" "regexp" "runtime" "strings" lua "github.com/yuin/gopher-lua" ) const ( configFileName = "config.lua" filesDirName = "files" ) type packageConfig struct { targets map[string]string disabled map[string]bool ignore []string compiledIgnores []*regexp.Regexp } var errTargetDisabled = errors.New("target disabled for this platform") func loadConfig(path string) (*packageConfig, error) { L := lua.NewState() defer L.Close() L.OpenLibs() if err := L.DoFile(path); err != nil { return nil, err } if L.GetTop() == 0 { return nil, errors.New("config.lua must return a table") } value := L.Get(-1) tbl, ok := value.(*lua.LTable) if !ok { return nil, errors.New("config.lua must return a table") } targetVal := tbl.RawGetString("target") targetTbl, ok := targetVal.(*lua.LTable) if !ok { return nil, errors.New("config.target must be a table") } targets := make(map[string]string) disabled := make(map[string]bool) targetTbl.ForEach(func(k, v lua.LValue) { ks, ok := k.(lua.LString) if !ok { return } if v == lua.LNil || v == lua.LFalse { disabled[string(ks)] = true return } vs, ok := v.(lua.LString) if !ok { return } targets[string(ks)] = expandHome(string(vs)) }) if len(targets) == 0 && len(disabled) == 0 { return nil, errors.New("config.target is empty") } ignore, err := parseIgnore(tbl) if err != nil { return nil, err } compiledIgnores, err := compileIgnorePatterns(ignore) if err != nil { return nil, err } return &packageConfig{targets: targets, disabled: disabled, ignore: ignore, compiledIgnores: compiledIgnores}, nil } func selectTarget(cfg *packageConfig) (string, error) { osKey := runtime.GOOS if osKey == "darwin" { osKey = "macos" } if cfg.disabled[osKey] { return "", errTargetDisabled } if target, ok := cfg.targets[osKey]; ok { return expandHome(target), nil } if target, ok := cfg.targets["default"]; ok { return expandHome(target), nil } return "", fmt.Errorf("missing target for %s and default", osKey) } func parseIgnore(cfgTbl *lua.LTable) ([]string, error) { ignoreVal := cfgTbl.RawGetString("ignore") if ignoreVal == lua.LNil { return nil, nil } ignoreTbl, ok := ignoreVal.(*lua.LTable) if !ok { return nil, errors.New("config.ignore must be an array of strings") } ignore := make([]string, 0, ignoreTbl.Len()) var parseErr error ignoreTbl.ForEach(func(k, v lua.LValue) { if parseErr != nil { return } if _, ok := k.(lua.LNumber); !ok { parseErr = errors.New("config.ignore must be an array of strings") return } s, ok := v.(lua.LString) if !ok { parseErr = errors.New("config.ignore must contain only strings") return } pattern := strings.TrimSpace(string(s)) if pattern == "" { parseErr = errors.New("config.ignore cannot contain empty patterns") return } ignore = append(ignore, pattern) }) if parseErr != nil { return nil, parseErr } return ignore, nil } func compileIgnorePatterns(patterns []string) ([]*regexp.Regexp, error) { compiled := make([]*regexp.Regexp, 0, len(patterns)) for _, pattern := range patterns { re, err := globToRegexp(pattern) if err != nil { return nil, fmt.Errorf("invalid ignore pattern %q: %w", pattern, err) } compiled = append(compiled, re) } return compiled, nil } func globToRegexp(pattern string) (*regexp.Regexp, error) { pattern = strings.ReplaceAll(pattern, "\\", "/") pattern = strings.TrimPrefix(pattern, "./") if strings.HasPrefix(pattern, "/") { pattern = strings.TrimPrefix(pattern, "/") } var b strings.Builder b.WriteString("^") for i := 0; i < len(pattern); { if i+1 < len(pattern) && pattern[i] == '*' && pattern[i+1] == '*' { b.WriteString(".*") i += 2 continue } ch := pattern[i] switch ch { case '*': b.WriteString("[^/]*") case '?': b.WriteString("[^/]") default: b.WriteString(regexp.QuoteMeta(string(ch))) } i++ } b.WriteString("$") return regexp.Compile(b.String()) } func shouldIgnorePath(rel string, cfg *packageConfig) bool { if cfg == nil || len(cfg.compiledIgnores) == 0 { return false } normalized := path.Clean(filepath.ToSlash(rel)) for _, re := range cfg.compiledIgnores { if re.MatchString(normalized) { return true } } return false } func writeConfig(path, targetRoot string) error { osKey := "linux" if runtime.GOOS == "darwin" { osKey = "macos" } prettyTarget := compressHome(targetRoot) content := fmt.Sprintf("---@class SigilConfig\n---@field target table\n---@field ignore? string[]\n\n---@type SigilConfig\nlocal config = {\n\ttarget = {\n\t\t%s = %q,\n\t\tdefault = %q,\n\t},\n\tignore = {\n\t\t-- \"**/.DS_Store\",\n\t\t-- \"**/*.tmp\",\n\t\t-- \"cache/**\",\n\t},\n}\n\nreturn config\n", osKey, prettyTarget, prettyTarget) return os.WriteFile(path, []byte(content), 0o644) }