package paths import ( "errors" "fmt" "os" "path/filepath" "strings" ) type Layout struct { ConfigHome string StateHome string CacheHome string RuntimeHome string ConfigDir string StateDir string CacheDir string RuntimeDir string SocketPath string DBPath string DaemonLog string VMsDir string ImagesDir string KernelsDir string OCICacheDir string SSHDir string KnownHostsPath string } func Resolve() (Layout, error) { home, err := os.UserHomeDir() if err != nil { return Layout{}, err } configHome := getenvDefault("XDG_CONFIG_HOME", filepath.Join(home, ".config")) stateHome := getenvDefault("XDG_STATE_HOME", filepath.Join(home, ".local", "state")) cacheHome := getenvDefault("XDG_CACHE_HOME", filepath.Join(home, ".cache")) runtimeHome := os.Getenv("XDG_RUNTIME_DIR") if runtimeHome == "" { runtimeHome = filepath.Join(os.TempDir(), fmt.Sprintf("banger-runtime-%d", os.Getuid())) } layout := Layout{ ConfigHome: configHome, StateHome: stateHome, CacheHome: cacheHome, RuntimeHome: runtimeHome, ConfigDir: filepath.Join(configHome, "banger"), StateDir: filepath.Join(stateHome, "banger"), CacheDir: filepath.Join(cacheHome, "banger"), RuntimeDir: filepath.Join(runtimeHome, "banger"), } layout.SocketPath = filepath.Join(layout.RuntimeDir, "bangerd.sock") layout.DBPath = filepath.Join(layout.StateDir, "state.db") layout.DaemonLog = filepath.Join(layout.StateDir, "bangerd.log") layout.VMsDir = filepath.Join(layout.StateDir, "vms") layout.ImagesDir = filepath.Join(layout.StateDir, "images") layout.KernelsDir = filepath.Join(layout.StateDir, "kernels") layout.OCICacheDir = filepath.Join(layout.CacheDir, "oci") layout.SSHDir = filepath.Join(layout.StateDir, "ssh") layout.KnownHostsPath = filepath.Join(layout.SSHDir, "known_hosts") return layout, nil } func Ensure(layout Layout) error { for _, dir := range []string{layout.ConfigDir, layout.StateDir, layout.CacheDir, layout.RuntimeDir, layout.VMsDir, layout.ImagesDir, layout.KernelsDir, layout.OCICacheDir} { if err := os.MkdirAll(dir, 0o755); err != nil { return err } } // SSH material (private key, known_hosts) — 0700 like ~/.ssh so // strict SSH clients don't complain and no other host user can // read it. Empty SSHDir means the caller built a Layout by hand // (tests) and doesn't need the subdir; skip silently. if strings.TrimSpace(layout.SSHDir) != "" { if err := os.MkdirAll(layout.SSHDir, 0o700); err != nil { return err } } return nil } var executablePath = os.Executable func BangerdPath() (string, error) { if env := os.Getenv("BANGER_DAEMON_BIN"); env != "" { return env, nil } exe, err := executablePath() if err != nil { return "", err } dir := filepath.Dir(exe) for _, candidate := range []string{ filepath.Join(dir, "bangerd"), filepath.Join(dir, "bangerd.exe"), } { if _, err := os.Stat(candidate); err == nil { return candidate, nil } } return "", errors.New("bangerd binary not found next to banger; run `make build`") } func CompanionBinaryPath(name string) (string, error) { envNames := []string{ "BANGER_" + strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(name)) + "_BIN", } if trimmed, ok := strings.CutPrefix(name, "banger-"); ok { envNames = append(envNames, "BANGER_"+strings.ToUpper(strings.NewReplacer("-", "_", ".", "_").Replace(trimmed))+"_BIN") } for _, envName := range envNames { if env := strings.TrimSpace(os.Getenv(envName)); env != "" { return env, nil } } exe, err := executablePath() if err != nil { return "", err } exeDir := filepath.Dir(exe) for _, candidate := range []string{ filepath.Join(exeDir, name), filepath.Join(exeDir, "..", "lib", "banger", name), filepath.Join(exeDir, "..", "libexec", "banger", name), } { if _, err := os.Stat(candidate); err == nil { return candidate, nil } } return "", fmt.Errorf("%s companion binary not found; run `make build` or reinstall banger", name) } func getenvDefault(key, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback }