package paths import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strconv" "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 } 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") return layout, nil } func Ensure(layout Layout) error { for _, dir := range []string{layout.ConfigDir, layout.StateDir, layout.CacheDir, layout.RuntimeDir, layout.VMsDir, layout.ImagesDir} { if err := os.MkdirAll(dir, 0o755); err != nil { return err } } return nil } func DetectRepoRoot() string { if env := os.Getenv("BANGER_REPO_ROOT"); env != "" { return env } candidates := []string{} if wd, err := os.Getwd(); err == nil { candidates = append(candidates, wd) } if exe, err := os.Executable(); err == nil { candidates = append(candidates, filepath.Dir(exe)) } if look, err := exec.LookPath("firecracker"); err == nil { candidates = append(candidates, filepath.Dir(look)) } for _, candidate := range candidates { if root := walkForRepoRoot(candidate); root != "" { return root } } return "" } func walkForRepoRoot(start string) string { current := start for { if hasRepoArtifacts(current) { return current } parent := filepath.Dir(current) if parent == current { return "" } current = parent } } func hasRepoArtifacts(dir string) bool { required := []string{"firecracker", "README.md"} for _, name := range required { if _, err := os.Stat(filepath.Join(dir, name)); err != nil { return false } } return true } func BangerdPath() (string, error) { if env := os.Getenv("BANGER_DAEMON_BIN"); env != "" { return env, nil } exe, err := os.Executable() 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 } } if root := DetectRepoRoot(); root != "" { for _, candidate := range []string{ filepath.Join(root, "bangerd"), filepath.Join(root, "bangerd.exe"), } { if _, err := os.Stat(candidate); err == nil { return candidate, nil } } } return "", errors.New("bangerd binary not found next to banger; build ./cmd/bangerd") } func getenvDefault(key, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback } func RuntimeFallbackLabel() string { return strconv.Itoa(os.Getuid()) }