Skip to content

Commit d43459b

Browse files
committed
feat(installer): add XDG Base Directory support on Linux
Add support for XDG_DATA_HOME on Linux following the XDG Base Directory Specification. This aligns dtvem with standard Linux conventions. Changes: - paths.go: Use $XDG_DATA_HOME/dtvem or ~/.local/share/dtvem on Linux - install.sh: Detect platform and use XDG paths on Linux - Add comprehensive documentation of path selection rationale - Add unit tests for XDG path selection logic Path Selection Summary: - Linux: $XDG_DATA_HOME/dtvem (or ~/.local/share/dtvem if unset) - macOS: ~/.dtvem (familiar to Unix CLI users) - Windows: %USERPROFILE%\.dtvem (visible and easy to backup) - Override: DTVEM_ROOT env var works on all platforms Closes #91
1 parent d4f8728 commit d43459b

3 files changed

Lines changed: 239 additions & 4 deletions

File tree

install.sh

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ set -e
55
# Usage: curl -fsSL https://raw.githubusercontent.com/dtvem/dtvem/main/install.sh | bash
66

77
REPO="dtvem/dtvem"
8-
INSTALL_DIR="$HOME/.dtvem/bin"
98

109
# This will be replaced with the actual version during release
1110
# Format: DTVEM_RELEASE_VERSION="1.0.0"
@@ -36,6 +35,33 @@ warning() {
3635
echo -e "${YELLOW}${NC} $1"
3736
}
3837

38+
# Get dtvem root directory
39+
# On Linux, respects XDG_DATA_HOME if set (defaults to ~/.local/share/dtvem)
40+
# On macOS, uses ~/.dtvem
41+
get_dtvem_root() {
42+
# Check for DTVEM_ROOT environment variable first (overrides all)
43+
if [ -n "$DTVEM_ROOT" ]; then
44+
echo "$DTVEM_ROOT"
45+
return
46+
fi
47+
48+
local os
49+
os=$(uname -s)
50+
51+
if [ "$os" = "Linux" ]; then
52+
# Respect XDG Base Directory specification
53+
if [ -n "$XDG_DATA_HOME" ]; then
54+
echo "$XDG_DATA_HOME/dtvem"
55+
else
56+
# XDG default: ~/.local/share
57+
echo "$HOME/.local/share/dtvem"
58+
fi
59+
else
60+
# macOS and others: use ~/.dtvem
61+
echo "$HOME/.dtvem"
62+
fi
63+
}
64+
3965
# Detect OS
4066
detect_os() {
4167
case "$(uname -s)" in
@@ -180,6 +206,12 @@ main() {
180206
ARCH=$(detect_arch)
181207
info "Detected platform: ${OS}-${ARCH}"
182208

209+
# Determine install directory based on platform and XDG
210+
DTVEM_ROOT=$(get_dtvem_root)
211+
INSTALL_DIR="$DTVEM_ROOT/bin"
212+
SHIMS_DIR="$DTVEM_ROOT/shims"
213+
info "Install directory: $INSTALL_DIR"
214+
183215
# Determine version to install
184216
local requested_version=""
185217
if [ -n "$DTVEM_VERSION" ]; then
@@ -336,7 +368,7 @@ main() {
336368
info "Running dtvem init to add shims directory to PATH..."
337369
if "$INSTALL_DIR/dtvem" init; then
338370
success "dtvem is ready to use!"
339-
info "Both ~/.dtvem/bin and ~/.dtvem/shims have been added to PATH"
371+
info "Both $INSTALL_DIR and $SHIMS_DIR have been added to PATH"
340372
else
341373
warning "dtvem init failed - you may need to run it manually"
342374
fi

src/internal/config/paths.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,29 @@ func initPaths() *Paths {
4545
}
4646
}
4747

48-
// getRootDir returns the root dtvem directory
48+
// getRootDir returns the root dtvem directory based on platform conventions.
49+
//
50+
// Path Selection Rationale:
51+
//
52+
// Linux: Follows XDG Base Directory Specification (https://specifications.freedesktop.org/basedir-spec/)
53+
// - Uses $XDG_DATA_HOME/dtvem if XDG_DATA_HOME is set
54+
// - Otherwise uses ~/.local/share/dtvem (XDG default)
55+
// - This is the standard location for user-specific data files on Linux
56+
//
57+
// macOS: Uses ~/.dtvem
58+
// - macOS has its own conventions (~/Library/Application Support) but many CLI tools
59+
// use dotfiles in home directory for better discoverability and Unix compatibility
60+
// - ~/.dtvem is more familiar to users coming from tools like nvm, pyenv, rbenv
61+
//
62+
// Windows: Uses %USERPROFILE%\.dtvem
63+
// - Alternatives considered: %LOCALAPPDATA% (C:\Users\<user>\AppData\Local)
64+
// - Chose home directory for consistency with macOS/Linux and better visibility
65+
// - Users expect CLI tool configs in their home directory
66+
// - Easier to locate and backup than buried in AppData
67+
//
68+
// Override: DTVEM_ROOT environment variable overrides all platform defaults
4969
func getRootDir() string {
50-
// Check for DTVEM_ROOT environment variable first
70+
// Check for DTVEM_ROOT environment variable first (overrides all)
5171
if root := os.Getenv("DTVEM_ROOT"); root != "" {
5272
return root
5373
}
@@ -59,9 +79,25 @@ func getRootDir() string {
5979
return ".dtvem"
6080
}
6181

82+
// On Linux, respect XDG Base Directory specification
83+
if runtime.GOOS == constants.OSLinux {
84+
return getXDGDataPath(home)
85+
}
86+
87+
// On macOS and Windows, use ~/.dtvem
6288
return filepath.Join(home, ".dtvem")
6389
}
6490

91+
// getXDGDataPath returns the XDG-compliant data path for dtvem on Linux
92+
// Uses XDG_DATA_HOME if set, otherwise defaults to ~/.local/share/dtvem
93+
func getXDGDataPath(home string) string {
94+
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
95+
return filepath.Join(xdgDataHome, "dtvem")
96+
}
97+
// XDG default: ~/.local/share
98+
return filepath.Join(home, ".local", "share", "dtvem")
99+
}
100+
65101
// RuntimeVersionPath returns the path to a specific runtime version
66102
func RuntimeVersionPath(runtimeName, version string) string {
67103
paths := DefaultPaths()

src/internal/config/paths_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88
"sync"
99
"testing"
10+
11+
"github.com/dtvem/dtvem/src/internal/constants"
1012
)
1113

1214
func TestGetPaths(t *testing.T) {
@@ -285,3 +287,168 @@ func TestDefaultPaths_ConcurrentAccess(t *testing.T) {
285287
t.Error("Root path is empty")
286288
}
287289
}
290+
291+
func TestGetXDGDataPath(t *testing.T) {
292+
home := "/home/testuser"
293+
294+
tests := []struct {
295+
name string
296+
xdgDataHome string
297+
expectedSuffix string
298+
}{
299+
{
300+
name: "XDG_DATA_HOME set",
301+
xdgDataHome: "/custom/data",
302+
expectedSuffix: filepath.Join("/custom/data", "dtvem"),
303+
},
304+
{
305+
name: "XDG_DATA_HOME empty - use default",
306+
xdgDataHome: "",
307+
expectedSuffix: filepath.Join(home, ".local", "share", "dtvem"),
308+
},
309+
}
310+
311+
for _, tt := range tests {
312+
t.Run(tt.name, func(t *testing.T) {
313+
// Save and restore XDG_DATA_HOME
314+
originalXDG := os.Getenv("XDG_DATA_HOME")
315+
defer func() {
316+
if originalXDG != "" {
317+
_ = os.Setenv("XDG_DATA_HOME", originalXDG)
318+
} else {
319+
_ = os.Unsetenv("XDG_DATA_HOME")
320+
}
321+
}()
322+
323+
if tt.xdgDataHome != "" {
324+
_ = os.Setenv("XDG_DATA_HOME", tt.xdgDataHome)
325+
} else {
326+
_ = os.Unsetenv("XDG_DATA_HOME")
327+
}
328+
329+
result := getXDGDataPath(home)
330+
if result != tt.expectedSuffix {
331+
t.Errorf("getXDGDataPath(%q) = %q, want %q", home, result, tt.expectedSuffix)
332+
}
333+
})
334+
}
335+
}
336+
337+
func TestGetRootDir_XDGOnLinux(t *testing.T) {
338+
// This test verifies the XDG behavior on Linux
339+
// On other platforms, it verifies that XDG is NOT used
340+
if runtime.GOOS != constants.OSLinux {
341+
t.Skip("XDG tests only run on Linux")
342+
}
343+
344+
// Save original environment
345+
originalRoot := os.Getenv("DTVEM_ROOT")
346+
originalXDG := os.Getenv("XDG_DATA_HOME")
347+
defer func() {
348+
if originalRoot != "" {
349+
_ = os.Setenv("DTVEM_ROOT", originalRoot)
350+
} else {
351+
_ = os.Unsetenv("DTVEM_ROOT")
352+
}
353+
if originalXDG != "" {
354+
_ = os.Setenv("XDG_DATA_HOME", originalXDG)
355+
} else {
356+
_ = os.Unsetenv("XDG_DATA_HOME")
357+
}
358+
resetPathsForTesting()
359+
}()
360+
361+
// Clear DTVEM_ROOT to test XDG behavior
362+
_ = os.Unsetenv("DTVEM_ROOT")
363+
364+
// Test with custom XDG_DATA_HOME
365+
customXDG := "/tmp/custom-xdg-data"
366+
_ = os.Setenv("XDG_DATA_HOME", customXDG)
367+
resetPathsForTesting()
368+
369+
result := getRootDir()
370+
expected := filepath.Join(customXDG, "dtvem")
371+
if result != expected {
372+
t.Errorf("getRootDir() with XDG_DATA_HOME=%q = %q, want %q", customXDG, result, expected)
373+
}
374+
375+
// Test with XDG_DATA_HOME unset (should use default)
376+
_ = os.Unsetenv("XDG_DATA_HOME")
377+
resetPathsForTesting()
378+
379+
result = getRootDir()
380+
home, _ := os.UserHomeDir()
381+
expected = filepath.Join(home, ".local", "share", "dtvem")
382+
if result != expected {
383+
t.Errorf("getRootDir() with XDG_DATA_HOME unset = %q, want %q", result, expected)
384+
}
385+
}
386+
387+
func TestGetRootDir_NonLinux(t *testing.T) {
388+
// On non-Linux platforms, verify that ~/.dtvem is used regardless of XDG
389+
if runtime.GOOS == constants.OSLinux {
390+
t.Skip("This test only runs on non-Linux platforms")
391+
}
392+
393+
// Save original environment
394+
originalRoot := os.Getenv("DTVEM_ROOT")
395+
originalXDG := os.Getenv("XDG_DATA_HOME")
396+
defer func() {
397+
if originalRoot != "" {
398+
_ = os.Setenv("DTVEM_ROOT", originalRoot)
399+
} else {
400+
_ = os.Unsetenv("DTVEM_ROOT")
401+
}
402+
if originalXDG != "" {
403+
_ = os.Setenv("XDG_DATA_HOME", originalXDG)
404+
} else {
405+
_ = os.Unsetenv("XDG_DATA_HOME")
406+
}
407+
resetPathsForTesting()
408+
}()
409+
410+
// Clear DTVEM_ROOT and set XDG_DATA_HOME
411+
_ = os.Unsetenv("DTVEM_ROOT")
412+
_ = os.Setenv("XDG_DATA_HOME", "/should/be/ignored")
413+
resetPathsForTesting()
414+
415+
result := getRootDir()
416+
home, _ := os.UserHomeDir()
417+
expected := filepath.Join(home, ".dtvem")
418+
419+
if result != expected {
420+
t.Errorf("getRootDir() on %s should ignore XDG_DATA_HOME, got %q, want %q",
421+
runtime.GOOS, result, expected)
422+
}
423+
}
424+
425+
func TestGetRootDir_DTVEMRootOverridesXDG(t *testing.T) {
426+
// Verify that DTVEM_ROOT takes precedence over XDG_DATA_HOME on all platforms
427+
originalRoot := os.Getenv("DTVEM_ROOT")
428+
originalXDG := os.Getenv("XDG_DATA_HOME")
429+
defer func() {
430+
if originalRoot != "" {
431+
_ = os.Setenv("DTVEM_ROOT", originalRoot)
432+
} else {
433+
_ = os.Unsetenv("DTVEM_ROOT")
434+
}
435+
if originalXDG != "" {
436+
_ = os.Setenv("XDG_DATA_HOME", originalXDG)
437+
} else {
438+
_ = os.Unsetenv("XDG_DATA_HOME")
439+
}
440+
resetPathsForTesting()
441+
}()
442+
443+
// Set both DTVEM_ROOT and XDG_DATA_HOME
444+
customRoot := "/custom/dtvem/root"
445+
_ = os.Setenv("DTVEM_ROOT", customRoot)
446+
_ = os.Setenv("XDG_DATA_HOME", "/should/be/ignored")
447+
resetPathsForTesting()
448+
449+
result := getRootDir()
450+
if result != customRoot {
451+
t.Errorf("getRootDir() with DTVEM_ROOT set should return DTVEM_ROOT, got %q, want %q",
452+
result, customRoot)
453+
}
454+
}

0 commit comments

Comments
 (0)