1package platform
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7)
8
9const devEnvironmentRootEnv = "VEV_ENV_ROOT"
10
11// ActivateDevelopmentEnvironment redirects vev's XDG roots into an isolated
12// development tree when VEV_ENV is set. The absolute .dev base is retained in
13// VEV_ENV_ROOT so re-executed roles do not rebase after changing directory.
14func ActivateDevelopmentEnvironment() error {
15 name := os.Getenv("VEV_ENV")
16 if name == "" {
17 return nil
18 }
19 if !validDevelopmentEnvironmentName(name) {
20 return fmt.Errorf("invalid VEV_ENV %q (want one safe segment matching [A-Za-z0-9][A-Za-z0-9._-]{0,63})", name)
21 }
22
23 base := os.Getenv(devEnvironmentRootEnv)
24 if base == "" {
25 absolute, err := filepath.Abs(".dev")
26 if err != nil {
27 return fmt.Errorf("resolve VEV_ENV %q root: %w", name, err)
28 }
29 base = absolute
30 } else if !filepath.IsAbs(base) {
31 return fmt.Errorf("invalid %s %q (want an absolute path)", devEnvironmentRootEnv, base)
32 }
33 root := filepath.Join(base, name)
34
35 values := map[string]string{
36 devEnvironmentRootEnv: base,
37 "XDG_CONFIG_HOME": filepath.Join(root, "config"),
38 "XDG_STATE_HOME": filepath.Join(root, "state"),
39 "XDG_RUNTIME_DIR": filepath.Join(root, "runtime"),
40 }
41 type previousValue struct {
42 value string
43 set bool
44 }
45 previous := make(map[string]previousValue, len(values))
46 for key := range values {
47 value, set := os.LookupEnv(key)
48 previous[key] = previousValue{value: value, set: set}
49 }
50 for key, value := range values {
51 if err := os.Setenv(key, value); err != nil {
52 for rollbackKey, old := range previous {
53 if old.set {
54 _ = os.Setenv(rollbackKey, old.value)
55 } else {
56 _ = os.Unsetenv(rollbackKey)
57 }
58 }
59 return fmt.Errorf("activate VEV_ENV %q: %w", name, err)
60 }
61 }
62 return nil
63}
64
65func validDevelopmentEnvironmentName(name string) bool {
66 if len(name) == 0 || len(name) > 64 || !asciiAlphaNumeric(name[0]) {
67 return false
68 }
69 for i := 1; i < len(name); i++ {
70 if !asciiAlphaNumeric(name[i]) && name[i] != '.' && name[i] != '_' && name[i] != '-' {
71 return false
72 }
73 }
74 return true
75}
76
77func asciiAlphaNumeric(value byte) bool {
78 return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' || value >= '0' && value <= '9'
79}
80
81// RuntimeDirFor returns the XDG_RUNTIME_DIR a vev process with environment
82// getenv and working directory cwd uses once VEV_ENV is activated. It lets a
83// caller resolve another process' runtime from its initial environment.
84func RuntimeDirFor(getenv func(string) string, cwd string) string {
85 name := getenv("VEV_ENV")
86 if name == "" || !validDevelopmentEnvironmentName(name) {
87 return getenv("XDG_RUNTIME_DIR")
88 }
89 base := getenv(devEnvironmentRootEnv)
90 if base == "" {
91 base = filepath.Join(cwd, ".dev")
92 }
93 return filepath.Join(base, name, "runtime")
94}
95
96// DevelopmentEnvironmentTempDir returns the private temporary-file directory
97// for an activated development environment. Ordinary invocations use the
98// existing os.TempDir fallback in the daemon.
99func DevelopmentEnvironmentTempDir() (string, bool) {
100 name := os.Getenv("VEV_ENV")
101 base := os.Getenv(devEnvironmentRootEnv)
102 if name == "" || base == "" {
103 return "", false
104 }
105 return filepath.Join(base, name, "tmp"), true
106}
107
108// StateDir returns the directory vev writes state to: $XDG_STATE_HOME/vev if
109// set, else ~/.local/state/vev.
110func StateDir() string {
111 if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" {
112 return filepath.Join(xdg, "vev")
113 }
114 home, err := os.UserHomeDir()
115 if err != nil || home == "" {
116 // Last resort: a per-uid temp path so state setup never aborts startup.
117 return filepath.Join(os.TempDir(), fmt.Sprintf("vev-state-%d", os.Getuid()))
118 }
119 return filepath.Join(home, ".local", "state", "vev")
120}
121
122// ConfigPath returns the user config file path: $XDG_CONFIG_HOME/vev/config if
123// set, else ~/.config/vev/config.
124func ConfigPath() string {
125 if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
126 return filepath.Join(xdg, "vev", "config")
127 }
128 home, err := os.UserHomeDir()
129 if err != nil || home == "" {
130 // Last resort: a per-uid temp path so config lookup never aborts startup.
131 return filepath.Join(os.TempDir(), fmt.Sprintf("vev-config-%d", os.Getuid()), "config")
132 }
133 return filepath.Join(home, ".config", "vev", "config")
134}
135
136// DirOrHome returns dir when it names an existing directory. Otherwise it
137// falls back to the current user's home directory, then to / as a last resort.
138func DirOrHome(dir string) string {
139 if dir != "" {
140 if st, err := os.Stat(dir); err == nil && st.IsDir() {
141 return dir
142 }
143 }
144 if home, err := os.UserHomeDir(); err == nil && home != "" {
145 return home
146 }
147 return "/"
148}