diff --git a/cmd/agent/workspace/up.go b/cmd/agent/workspace/up.go index 9dab9e938..888e13f15 100644 --- a/cmd/agent/workspace/up.go +++ b/cmd/agent/workspace/up.go @@ -24,6 +24,7 @@ import ( "github.com/loft-sh/devpod/pkg/dockercredentials" "github.com/loft-sh/devpod/pkg/extract" provider2 "github.com/loft-sh/devpod/pkg/provider" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/devpod/scripts" "github.com/loft-sh/log" "github.com/pkg/errors" @@ -451,7 +452,7 @@ func configureDockerDaemon(ctx context.Context, log log.Logger) (err error) { } }`) // Check rootless docker - homeDir, err := os.UserHomeDir() + homeDir, err := util.UserHomeDir() if err != nil { return err } diff --git a/cmd/pro/start.go b/cmd/pro/start.go index 86653b234..f108d2137 100644 --- a/cmd/pro/start.go +++ b/cmd/pro/start.go @@ -21,7 +21,6 @@ import ( "github.com/denisbrodbeck/machineid" jsonpatch "github.com/evanphx/json-patch" "github.com/mgutz/ansi" - "github.com/mitchellh/go-homedir" "github.com/skratchdot/open-golang/open" storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1" @@ -30,6 +29,7 @@ import ( proflags "github.com/loft-sh/devpod/cmd/pro/flags" "github.com/loft-sh/devpod/pkg/platform" "github.com/loft-sh/devpod/pkg/platform/client" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" "github.com/loft-sh/log/hash" "github.com/loft-sh/log/scanner" @@ -1693,7 +1693,7 @@ func getMachineUID(log log.Logger) string { } // get $HOME to distinguish two users on the same machine // will be hashed later together with the ID - home, err := homedir.Dir() + home, err := util.UserHomeDir() if err != nil { home = "error" if log != nil { diff --git a/go.mod b/go.mod index cc5dc7de7..db7ae695b 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( github.com/loft-sh/ssh v0.0.5 github.com/mattn/go-isatty v0.0.20 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d - github.com/mitchellh/go-homedir v1.1.0 github.com/moby/buildkit v0.18.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 @@ -160,6 +159,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect diff --git a/pkg/agent/workspace.go b/pkg/agent/workspace.go index 5b15dd50c..4c11ed522 100644 --- a/pkg/agent/workspace.go +++ b/pkg/agent/workspace.go @@ -15,8 +15,8 @@ import ( "github.com/loft-sh/devpod/pkg/git" "github.com/loft-sh/devpod/pkg/gitcredentials" provider2 "github.com/loft-sh/devpod/pkg/provider" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" "github.com/moby/patternmatcher/ignorefile" ) @@ -50,7 +50,7 @@ func findDir(agentFolder string, validate func(path string) bool) string { } // check home folder first - homeDir, _ := homedir.Dir() + homeDir, _ := util.UserHomeDir() if homeDir != "" { homeDir = filepath.Join(homeDir, ".devpod", "agent") if validate(homeDir) { diff --git a/pkg/command/user.go b/pkg/command/user.go index 410c8c6da..5c00a1a93 100644 --- a/pkg/command/user.go +++ b/pkg/command/user.go @@ -3,12 +3,12 @@ package command import ( "os/user" - "github.com/mitchellh/go-homedir" + "github.com/loft-sh/devpod/pkg/util" ) func GetHome(userName string) (string, error) { if userName == "" { - return homedir.Dir() + return util.UserHomeDir() } u, err := user.Lookup(userName) diff --git a/pkg/config/dir.go b/pkg/config/dir.go index 85035fa4e..fe844c74c 100644 --- a/pkg/config/dir.go +++ b/pkg/config/dir.go @@ -4,7 +4,7 @@ import ( "os" "path/filepath" - homedir "github.com/mitchellh/go-homedir" + "github.com/loft-sh/devpod/pkg/util" ) // Override devpod home @@ -19,7 +19,7 @@ func GetConfigDir() (string, error) { return homeDir, nil } - homeDir, err := homedir.Dir() + homeDir, err := util.UserHomeDir() if err != nil { return "", err } diff --git a/pkg/encoding/encoding.go b/pkg/encoding/encoding.go index cce2df736..4b3b3af0c 100644 --- a/pkg/encoding/encoding.go +++ b/pkg/encoding/encoding.go @@ -9,8 +9,8 @@ import ( "github.com/denisbrodbeck/machineid" "github.com/google/uuid" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" ) const ( @@ -81,7 +81,7 @@ func GetMachineUID(log log.Logger) string { } // get $HOME to distinguish two users on the same machine // will be hashed later together with the ID - home, err := homedir.Dir() + home, err := util.UserHomeDir() if err != nil { home = "error" if log != nil { diff --git a/pkg/ide/fleet/fleet.go b/pkg/ide/fleet/fleet.go index e47feea17..df7e95731 100644 --- a/pkg/ide/fleet/fleet.go +++ b/pkg/ide/fleet/fleet.go @@ -16,9 +16,9 @@ import ( devpodhttp "github.com/loft-sh/devpod/pkg/http" "github.com/loft-sh/devpod/pkg/ide" "github.com/loft-sh/devpod/pkg/single" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" "github.com/loft-sh/log/scanner" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" ) @@ -215,7 +215,7 @@ func prepareFleetServerLocation(userName string) (string, error) { if userName != "" { homeFolder, err = command.GetHome(userName) } else { - homeFolder, err = homedir.Dir() + homeFolder, err = util.UserHomeDir() } if err != nil { return "", err diff --git a/pkg/ide/jetbrains/generic.go b/pkg/ide/jetbrains/generic.go index 06d7c9e54..606b629b6 100644 --- a/pkg/ide/jetbrains/generic.go +++ b/pkg/ide/jetbrains/generic.go @@ -17,8 +17,8 @@ import ( "github.com/loft-sh/devpod/pkg/extract" devpodhttp "github.com/loft-sh/devpod/pkg/http" "github.com/loft-sh/devpod/pkg/ide" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/skratchdot/open-golang/open" ) @@ -134,7 +134,7 @@ func getBaseFolder(userName string) (string, error) { if userName != "" { homeFolder, err = command.GetHome(userName) } else { - homeFolder, err = homedir.Dir() + homeFolder, err = util.UserHomeDir() } if err != nil { return "", err diff --git a/pkg/ide/openvscode/openvscode.go b/pkg/ide/openvscode/openvscode.go index 43ad96aed..d209d3cf0 100644 --- a/pkg/ide/openvscode/openvscode.go +++ b/pkg/ide/openvscode/openvscode.go @@ -16,8 +16,8 @@ import ( "github.com/loft-sh/devpod/pkg/ide" "github.com/loft-sh/devpod/pkg/ide/vscode" "github.com/loft-sh/devpod/pkg/single" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -280,7 +280,7 @@ func prepareOpenVSCodeServerLocation(userName string) (string, error) { if userName != "" { homeFolder, err = command.GetHome(userName) } else { - homeFolder, err = homedir.Dir() + homeFolder, err = util.UserHomeDir() } if err != nil { return "", err diff --git a/pkg/ide/vscode/vscode.go b/pkg/ide/vscode/vscode.go index 462d3a947..641600089 100644 --- a/pkg/ide/vscode/vscode.go +++ b/pkg/ide/vscode/vscode.go @@ -13,8 +13,8 @@ import ( "github.com/loft-sh/devpod/pkg/config" copy2 "github.com/loft-sh/devpod/pkg/copy" "github.com/loft-sh/devpod/pkg/ide" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -421,7 +421,7 @@ func prepareServerLocation(userName string, create bool, flavor Flavor) (string, if userName != "" { homeFolder, err = command.GetHome(userName) } else { - homeFolder, err = homedir.Dir() + homeFolder, err = util.UserHomeDir() } if err != nil { return "", err diff --git a/pkg/platform/client/client.go b/pkg/platform/client/client.go index f96956b9b..397e71c3c 100644 --- a/pkg/platform/client/client.go +++ b/pkg/platform/client/client.go @@ -24,9 +24,9 @@ import ( "github.com/loft-sh/devpod/pkg/platform/kube" "github.com/loft-sh/devpod/pkg/platform/project" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/devpod/pkg/version" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" perrors "github.com/pkg/errors" "github.com/skratchdot/open-golang/open" "k8s.io/client-go/rest" @@ -48,7 +48,7 @@ const ( ) func init() { - hd, _ := homedir.Dir() + hd, _ := util.UserHomeDir() if folder, ok := os.LookupEnv("LOFT_CACHE_FOLDER"); ok { CacheFolder = filepath.Join(hd, folder) } else { diff --git a/pkg/ssh/config.go b/pkg/ssh/config.go index e7ee2329a..3535baf60 100644 --- a/pkg/ssh/config.go +++ b/pkg/ssh/config.go @@ -11,9 +11,9 @@ import ( "strings" "sync" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" "github.com/loft-sh/log/scanner" - "github.com/mitchellh/go-homedir" "github.com/pkg/errors" ) @@ -186,7 +186,7 @@ func writeSSHConfig(path, content string, log log.Logger) error { } func ResolveSSHConfigPath(sshConfigPath string) (string, error) { - homeDir, err := homedir.Dir() + homeDir, err := util.UserHomeDir() if err != nil { return "", errors.Wrap(err, "get home dir") } diff --git a/pkg/ssh/keys.go b/pkg/ssh/keys.go index fabeeaab3..edd5e072c 100644 --- a/pkg/ssh/keys.go +++ b/pkg/ssh/keys.go @@ -12,7 +12,7 @@ import ( "sync" "github.com/loft-sh/devpod/pkg/provider" - "github.com/mitchellh/go-homedir" + "github.com/loft-sh/devpod/pkg/util" "github.com/pkg/errors" "golang.org/x/crypto/ssh" @@ -80,7 +80,7 @@ func GetPrivateKeyRaw(context, workspaceID string) ([]byte, error) { } func GetDevPodKeysDir() string { - dir, err := homedir.Dir() + dir, err := util.UserHomeDir() if err == nil { tempDir := filepath.Join(dir, ".devpod", "keys") err = os.MkdirAll(tempDir, 0755) diff --git a/pkg/ssh/ssh_add.go b/pkg/ssh/ssh_add.go index 6072928a4..572b56fc5 100644 --- a/pkg/ssh/ssh_add.go +++ b/pkg/ssh/ssh_add.go @@ -10,8 +10,8 @@ import ( "github.com/loft-sh/devpod/pkg/command" devsshagent "github.com/loft-sh/devpod/pkg/ssh/agent" + "github.com/loft-sh/devpod/pkg/util" "github.com/loft-sh/log" - "github.com/mitchellh/go-homedir" "golang.org/x/crypto/ssh" ) @@ -41,7 +41,7 @@ func AddPrivateKeysToAgent(ctx context.Context, log log.Logger) error { } func FindPrivateKeys() ([]string, error) { - homeDir, err := homedir.Dir() + homeDir, err := util.UserHomeDir() if err != nil { return nil, err } diff --git a/pkg/telemetry/helpers.go b/pkg/telemetry/helpers.go index e3e61fe9d..82a819d87 100644 --- a/pkg/telemetry/helpers.go +++ b/pkg/telemetry/helpers.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/denisbrodbeck/machineid" - "github.com/mitchellh/go-homedir" + "github.com/loft-sh/devpod/pkg/util" ) // GetMachineID retrieves machine ID and encodes it together with users $HOME path and @@ -19,7 +19,7 @@ func GetMachineID() string { // get $HOME to distinguish two users on the same machine // will be hashed later together with the ID - home, err := homedir.Dir() + home, err := util.UserHomeDir() if err != nil { home = "error" } diff --git a/pkg/util/homedir.go b/pkg/util/homedir.go new file mode 100644 index 000000000..ff6f34f3b --- /dev/null +++ b/pkg/util/homedir.go @@ -0,0 +1,90 @@ +package util + +import ( + "bytes" + "errors" + "os" + "os/exec" + "runtime" + "strconv" + "strings" +) + +// UserHomeDir returns the home directory for the executing user. +// +// This extends the logic of os.UserHomeDir() with the now archived package +// github.com/mitchellh/go-homedir for compatibility. +func UserHomeDir() (string, error) { + // Always try the HOME environment variable first + homeEnv := "HOME" + if runtime.GOOS == "plan9" { + homeEnv = "home" + } + if home := os.Getenv(homeEnv); home != "" { + return home, nil + } + + // Rely on os.UserHomeDir() here, as it's the standard method moving forward + if home, _ := os.UserHomeDir(); home != "" { + return home, nil + } + + var stdout bytes.Buffer + + // Finally, handle cases existed in go-homedir but not in the current + // os.UserHomeDir() implementation + switch runtime.GOOS { + case "windows": + drive := os.Getenv("HOMEDRIVE") + path := os.Getenv("HOMEPATH") + if drive == "" || path == "" { + return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank") + } + return drive + path, nil + case "darwin": + cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`) + cmd.Stdout = &stdout + if err := cmd.Run(); err == nil { + result := strings.TrimSpace(stdout.String()) + if result != "" { + return result, nil + } + } + default: + cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid())) + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + // If the error is ErrNotFound, we ignore it. Otherwise, return it. + if errors.Is(err, exec.ErrNotFound) { + return "", err + } + } else { + if passwd := strings.TrimSpace(stdout.String()); passwd != "" { + // username:password:uid:gid:gecos:home:shell + passwdParts := strings.SplitN(passwd, ":", 7) + if len(passwdParts) > 5 { + return passwdParts[5], nil + } + } + } + } + + // If all else fails, try the shell + if runtime.GOOS != "windows" { + stdout.Reset() + cmd := exec.Command("sh", "-c", "cd && pwd") + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return "", err + } + + result := strings.TrimSpace(stdout.String()) + if result == "" { + return "", errors.New("blank output when reading home directory") + } + + return result, nil + } + + return "", errors.New("can't determine the home directory") +} diff --git a/pkg/util/homedir_test.go b/pkg/util/homedir_test.go new file mode 100644 index 000000000..b7a32f8e0 --- /dev/null +++ b/pkg/util/homedir_test.go @@ -0,0 +1,63 @@ +package util + +import ( + "os" + "runtime" + "testing" + + "gotest.tools/assert" +) + +func TestUserHomeDir(t *testing.T) { + // Remember to reset environment variables after the test + origHome := os.Getenv("HOME") + origUserProfile := os.Getenv("USERPROFILE") + t.Cleanup(func() { + os.Setenv("HOME", origHome) + os.Setenv("USERPROFILE", origUserProfile) + }) + + type input struct { + home, userProfile string + } + + type testCase struct { + Name string + Input input + Expect string + } + + testCases := []testCase{ + { + // $HOME is preferred on every platform + Name: "both HOME and USERPROFILE are set", + Input: input{ + home: "home", + userProfile: "userProfile", + }, + Expect: "home", + }, + } + if runtime.GOOS == "windows" { + // On Windows, after $HOME, %userprofile% value is checked + testCases = append(testCases, testCase{ + Name: "HOME is unset and USERPROFILE is set", + Input: input{ + home: "", + userProfile: "userProfile", + }, + Expect: "userProfile", + }) + } + + for _, test := range testCases { + t.Run(test.Name, func(t *testing.T) { + os.Setenv("HOME", test.Input.home) + os.Setenv("USERPROFILE", test.Input.userProfile) + + got, err := UserHomeDir() + assert.NilError(t, err, test.Name) + assert.Equal(t, test.Expect, got) + }) + } +}