diff --git a/install.sh b/install.sh index 321ecd2..1070a00 100644 --- a/install.sh +++ b/install.sh @@ -5,7 +5,6 @@ set -e # Usage: curl -fsSL https://raw.githubusercontent.com/dtvem/dtvem/main/install.sh | bash REPO="dtvem/dtvem" -INSTALL_DIR="$HOME/.dtvem/bin" # This will be replaced with the actual version during release # Format: DTVEM_RELEASE_VERSION="1.0.0" @@ -36,6 +35,33 @@ warning() { echo -e "${YELLOW}⚠${NC} $1" } +# Get dtvem root directory +# On Linux, respects XDG_DATA_HOME if set (defaults to ~/.local/share/dtvem) +# On macOS, uses ~/.dtvem +get_dtvem_root() { + # Check for DTVEM_ROOT environment variable first (overrides all) + if [ -n "$DTVEM_ROOT" ]; then + echo "$DTVEM_ROOT" + return + fi + + local os + os=$(uname -s) + + if [ "$os" = "Linux" ]; then + # Respect XDG Base Directory specification + if [ -n "$XDG_DATA_HOME" ]; then + echo "$XDG_DATA_HOME/dtvem" + else + # XDG default: ~/.local/share + echo "$HOME/.local/share/dtvem" + fi + else + # macOS and others: use ~/.dtvem + echo "$HOME/.dtvem" + fi +} + # Detect OS detect_os() { case "$(uname -s)" in @@ -180,6 +206,12 @@ main() { ARCH=$(detect_arch) info "Detected platform: ${OS}-${ARCH}" + # Determine install directory based on platform and XDG + DTVEM_ROOT=$(get_dtvem_root) + INSTALL_DIR="$DTVEM_ROOT/bin" + SHIMS_DIR="$DTVEM_ROOT/shims" + info "Install directory: $INSTALL_DIR" + # Determine version to install local requested_version="" if [ -n "$DTVEM_VERSION" ]; then @@ -336,7 +368,7 @@ main() { info "Running dtvem init to add shims directory to PATH..." if "$INSTALL_DIR/dtvem" init; then success "dtvem is ready to use!" - info "Both ~/.dtvem/bin and ~/.dtvem/shims have been added to PATH" + info "Both $INSTALL_DIR and $SHIMS_DIR have been added to PATH" else warning "dtvem init failed - you may need to run it manually" fi diff --git a/src/internal/config/paths.go b/src/internal/config/paths.go index 9827962..a2cf3b4 100644 --- a/src/internal/config/paths.go +++ b/src/internal/config/paths.go @@ -45,9 +45,29 @@ func initPaths() *Paths { } } -// getRootDir returns the root dtvem directory +// getRootDir returns the root dtvem directory based on platform conventions. +// +// Path Selection Rationale: +// +// Linux: Follows XDG Base Directory Specification (https://specifications.freedesktop.org/basedir-spec/) +// - Uses $XDG_DATA_HOME/dtvem if XDG_DATA_HOME is set +// - Otherwise uses ~/.local/share/dtvem (XDG default) +// - This is the standard location for user-specific data files on Linux +// +// macOS: Uses ~/.dtvem +// - macOS has its own conventions (~/Library/Application Support) but many CLI tools +// use dotfiles in home directory for better discoverability and Unix compatibility +// - ~/.dtvem is more familiar to users coming from tools like nvm, pyenv, rbenv +// +// Windows: Uses %USERPROFILE%\.dtvem +// - Alternatives considered: %LOCALAPPDATA% (C:\Users\\AppData\Local) +// - Chose home directory for consistency with macOS/Linux and better visibility +// - Users expect CLI tool configs in their home directory +// - Easier to locate and backup than buried in AppData +// +// Override: DTVEM_ROOT environment variable overrides all platform defaults func getRootDir() string { - // Check for DTVEM_ROOT environment variable first + // Check for DTVEM_ROOT environment variable first (overrides all) if root := os.Getenv("DTVEM_ROOT"); root != "" { return root } @@ -59,9 +79,25 @@ func getRootDir() string { return ".dtvem" } + // On Linux, respect XDG Base Directory specification + if runtime.GOOS == constants.OSLinux { + return getXDGDataPath(home) + } + + // On macOS and Windows, use ~/.dtvem return filepath.Join(home, ".dtvem") } +// getXDGDataPath returns the XDG-compliant data path for dtvem on Linux +// Uses XDG_DATA_HOME if set, otherwise defaults to ~/.local/share/dtvem +func getXDGDataPath(home string) string { + if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" { + return filepath.Join(xdgDataHome, "dtvem") + } + // XDG default: ~/.local/share + return filepath.Join(home, ".local", "share", "dtvem") +} + // RuntimeVersionPath returns the path to a specific runtime version func RuntimeVersionPath(runtimeName, version string) string { paths := DefaultPaths() diff --git a/src/internal/config/paths_test.go b/src/internal/config/paths_test.go index d4ed2b0..c119c39 100644 --- a/src/internal/config/paths_test.go +++ b/src/internal/config/paths_test.go @@ -7,6 +7,8 @@ import ( "strings" "sync" "testing" + + "github.com/dtvem/dtvem/src/internal/constants" ) func TestGetPaths(t *testing.T) { @@ -285,3 +287,168 @@ func TestDefaultPaths_ConcurrentAccess(t *testing.T) { t.Error("Root path is empty") } } + +func TestGetXDGDataPath(t *testing.T) { + home := "/home/testuser" + + tests := []struct { + name string + xdgDataHome string + expectedSuffix string + }{ + { + name: "XDG_DATA_HOME set", + xdgDataHome: "/custom/data", + expectedSuffix: filepath.Join("/custom/data", "dtvem"), + }, + { + name: "XDG_DATA_HOME empty - use default", + xdgDataHome: "", + expectedSuffix: filepath.Join(home, ".local", "share", "dtvem"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save and restore XDG_DATA_HOME + originalXDG := os.Getenv("XDG_DATA_HOME") + defer func() { + if originalXDG != "" { + _ = os.Setenv("XDG_DATA_HOME", originalXDG) + } else { + _ = os.Unsetenv("XDG_DATA_HOME") + } + }() + + if tt.xdgDataHome != "" { + _ = os.Setenv("XDG_DATA_HOME", tt.xdgDataHome) + } else { + _ = os.Unsetenv("XDG_DATA_HOME") + } + + result := getXDGDataPath(home) + if result != tt.expectedSuffix { + t.Errorf("getXDGDataPath(%q) = %q, want %q", home, result, tt.expectedSuffix) + } + }) + } +} + +func TestGetRootDir_XDGOnLinux(t *testing.T) { + // This test verifies the XDG behavior on Linux + // On other platforms, it verifies that XDG is NOT used + if runtime.GOOS != constants.OSLinux { + t.Skip("XDG tests only run on Linux") + } + + // Save original environment + originalRoot := os.Getenv("DTVEM_ROOT") + originalXDG := os.Getenv("XDG_DATA_HOME") + defer func() { + if originalRoot != "" { + _ = os.Setenv("DTVEM_ROOT", originalRoot) + } else { + _ = os.Unsetenv("DTVEM_ROOT") + } + if originalXDG != "" { + _ = os.Setenv("XDG_DATA_HOME", originalXDG) + } else { + _ = os.Unsetenv("XDG_DATA_HOME") + } + resetPathsForTesting() + }() + + // Clear DTVEM_ROOT to test XDG behavior + _ = os.Unsetenv("DTVEM_ROOT") + + // Test with custom XDG_DATA_HOME + customXDG := "/tmp/custom-xdg-data" + _ = os.Setenv("XDG_DATA_HOME", customXDG) + resetPathsForTesting() + + result := getRootDir() + expected := filepath.Join(customXDG, "dtvem") + if result != expected { + t.Errorf("getRootDir() with XDG_DATA_HOME=%q = %q, want %q", customXDG, result, expected) + } + + // Test with XDG_DATA_HOME unset (should use default) + _ = os.Unsetenv("XDG_DATA_HOME") + resetPathsForTesting() + + result = getRootDir() + home, _ := os.UserHomeDir() + expected = filepath.Join(home, ".local", "share", "dtvem") + if result != expected { + t.Errorf("getRootDir() with XDG_DATA_HOME unset = %q, want %q", result, expected) + } +} + +func TestGetRootDir_NonLinux(t *testing.T) { + // On non-Linux platforms, verify that ~/.dtvem is used regardless of XDG + if runtime.GOOS == constants.OSLinux { + t.Skip("This test only runs on non-Linux platforms") + } + + // Save original environment + originalRoot := os.Getenv("DTVEM_ROOT") + originalXDG := os.Getenv("XDG_DATA_HOME") + defer func() { + if originalRoot != "" { + _ = os.Setenv("DTVEM_ROOT", originalRoot) + } else { + _ = os.Unsetenv("DTVEM_ROOT") + } + if originalXDG != "" { + _ = os.Setenv("XDG_DATA_HOME", originalXDG) + } else { + _ = os.Unsetenv("XDG_DATA_HOME") + } + resetPathsForTesting() + }() + + // Clear DTVEM_ROOT and set XDG_DATA_HOME + _ = os.Unsetenv("DTVEM_ROOT") + _ = os.Setenv("XDG_DATA_HOME", "/should/be/ignored") + resetPathsForTesting() + + result := getRootDir() + home, _ := os.UserHomeDir() + expected := filepath.Join(home, ".dtvem") + + if result != expected { + t.Errorf("getRootDir() on %s should ignore XDG_DATA_HOME, got %q, want %q", + runtime.GOOS, result, expected) + } +} + +func TestGetRootDir_DTVEMRootOverridesXDG(t *testing.T) { + // Verify that DTVEM_ROOT takes precedence over XDG_DATA_HOME on all platforms + originalRoot := os.Getenv("DTVEM_ROOT") + originalXDG := os.Getenv("XDG_DATA_HOME") + defer func() { + if originalRoot != "" { + _ = os.Setenv("DTVEM_ROOT", originalRoot) + } else { + _ = os.Unsetenv("DTVEM_ROOT") + } + if originalXDG != "" { + _ = os.Setenv("XDG_DATA_HOME", originalXDG) + } else { + _ = os.Unsetenv("XDG_DATA_HOME") + } + resetPathsForTesting() + }() + + // Set both DTVEM_ROOT and XDG_DATA_HOME + customRoot := "/custom/dtvem/root" + _ = os.Setenv("DTVEM_ROOT", customRoot) + _ = os.Setenv("XDG_DATA_HOME", "/should/be/ignored") + resetPathsForTesting() + + result := getRootDir() + if result != customRoot { + t.Errorf("getRootDir() with DTVEM_ROOT set should return DTVEM_ROOT, got %q, want %q", + result, customRoot) + } +}