Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 38 additions & 2 deletions src/internal/config/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -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\<user>\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
}
Expand All @@ -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()
Expand Down
167 changes: 167 additions & 0 deletions src/internal/config/paths_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"
"sync"
"testing"

"github.com/dtvem/dtvem/src/internal/constants"
)

func TestGetPaths(t *testing.T) {
Expand Down Expand Up @@ -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)
}
}