-
Notifications
You must be signed in to change notification settings - Fork 184
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
shell,nix: fix shell PATH to prefer nix packages (#51)
This fixes an issue where the user's rc (~/.bashrc, ~/.zshrc, etc.) files were prepending to the devbox shell PATH. This can cause the shell to unexpectedly run the non-devbox version of programs. It's especially noticeable with some version managers that point the PATH at their shims so they can control which version of a program is run. The basic steps that result in this issue are: 1. Devbox launches Nix, which sets the PATH to point to the Nix store. 2. From within Nix, we launch the user's default shell. 3. The shell runs the user's init files and stomps on the Nix PATH. Fixing this is a little tricky because there's no straightforward way to run additional commands _after_ the shell runs the init files. Instead, we need to copy the user's init files and append to them. Doing this requires knowing how each shell handles initialization, so we need to special-case the more common shells by performing the following steps: 1. Attempt to detect the user's default shell via the SHELL env var (we may add more sophisticated detection later). 2. If the shell is recognized as bash, zsh, ksh, dash, ash or sh, copy over the user's corresponding init files to a temp directory and append our own commands to it. 3. Build an `exec` shell command that invokes the new shell and tells it to run our temp init file instead of the user's. If we can't detect the shell, we fall back to launching a vanilla Nix shell. If we don't recognize the detected shell, we fall back to launching it without overriding the init files. Finally, since we now have a way of running our own commands in the user's shell, we can change the PS1 prompt to show the user that they're in devbox. Fixes #17, #25, #44, #46.
- Loading branch information
Showing
4 changed files
with
207 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved. | ||
// Use of this source code is governed by the license in the LICENSE file. | ||
|
||
// Package shell detects the user's default shell and configures it to run in | ||
// Devbox. | ||
package shell | ||
|
||
import ( | ||
"bytes" | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
) | ||
|
||
type name string | ||
|
||
const ( | ||
shUnknown name = "" | ||
shBash name = "bash" | ||
shZsh name = "zsh" | ||
shKsh name = "ksh" | ||
shPosix name = "posix" | ||
) | ||
|
||
// Shell configures a user's shell to run in Devbox. | ||
type Shell struct { | ||
name name | ||
path string | ||
initFile string | ||
devboxInitFile string | ||
} | ||
|
||
// Detect attempts to determine the user's default shell. | ||
func Detect() (*Shell, error) { | ||
path := os.Getenv("SHELL") | ||
if path == "" { | ||
return nil, errors.New("unable to detect the current shell") | ||
} | ||
|
||
sh := &Shell{path: filepath.Clean(path)} | ||
base := filepath.Base(path) | ||
// Login shell | ||
if base[0] == '-' { | ||
base = base[1:] | ||
} | ||
switch base { | ||
case "bash": | ||
sh.name = shBash | ||
sh.initFile = rcfilePath(".bashrc") | ||
case "zsh": | ||
sh.name = shZsh | ||
sh.initFile = rcfilePath(".zshrc") | ||
case "ksh": | ||
sh.name = shKsh | ||
sh.initFile = rcfilePath(".kshrc") | ||
case "dash", "ash", "sh": | ||
sh.name = shPosix | ||
sh.initFile = os.Getenv("ENV") | ||
|
||
// Just make up a name if there isn't already an init file set | ||
// so we have somewhere to put a new one. | ||
if sh.initFile == "" { | ||
sh.initFile = ".shinit" | ||
} | ||
default: | ||
sh.name = shUnknown | ||
} | ||
return sh, nil | ||
} | ||
|
||
// rcfilePath returns the absolute path for an rcfile, which is usually in the | ||
// user's home directory. It doesn't guarantee that the file exists. | ||
func rcfilePath(basename string) string { | ||
home, err := os.UserHomeDir() | ||
if err != nil { | ||
return "" | ||
} | ||
return filepath.Join(home, basename) | ||
} | ||
|
||
// SetInit configures the shell to run a script at startup. The script runs | ||
// after the user's usual init files. The script's environment will contain an | ||
// ORIGINAL_PATH environment variable, which will bet set to the PATH before | ||
// the user's init files have had a chance to modify it. | ||
func (s *Shell) SetInit(script string) error { | ||
script = strings.TrimSpace(script) | ||
if script == "" { | ||
return nil | ||
} | ||
|
||
initFile, _ := os.ReadFile(s.initFile) | ||
initFile = bytes.TrimSpace(initFile) | ||
if len(initFile) > 0 { | ||
initFile = append(initFile, '\n', '\n') | ||
} | ||
|
||
buf := bytes.NewBuffer(initFile) | ||
buf.WriteString(` | ||
# Begin Devbox Shell Hook | ||
`) | ||
buf.WriteString(script) | ||
buf.WriteString(` | ||
# End Devbox Shell Hook | ||
`) | ||
|
||
// We need a temp dir (as opposed to a temp file) because zsh uses | ||
// ZDOTDIR to point to a new directory containing the .zshrc. | ||
tmp, err := os.MkdirTemp("", "devbox") | ||
if err != nil { | ||
return fmt.Errorf("create temp dir for shell init file: %v", err) | ||
} | ||
devboxInitFile := filepath.Join(tmp, filepath.Base(s.initFile)) | ||
if err := os.WriteFile(devboxInitFile, buf.Bytes(), 0600); err != nil { | ||
return fmt.Errorf("write to shell init file: %v", err) | ||
} | ||
s.devboxInitFile = devboxInitFile | ||
return nil | ||
} | ||
|
||
// ExecCommand is a command that replaces the current shell with s. | ||
func (s *Shell) ExecCommand() string { | ||
if s.devboxInitFile == "" { | ||
return "exec " + s.path | ||
} | ||
|
||
switch s.name { | ||
case shBash: | ||
return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" %s --rcfile "%s"`, | ||
os.Getenv("PATH"), s.path, s.devboxInitFile) | ||
case shZsh: | ||
return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" ZDOTDIR="%s" %s`, | ||
os.Getenv("PATH"), filepath.Dir(s.devboxInitFile), s.path) | ||
case shKsh, shPosix: | ||
return fmt.Sprintf(`exec /usr/bin/env ORIGINAL_PATH="%s" ENV="%s" %s `, | ||
os.Getenv("PATH"), s.devboxInitFile, s.path) | ||
default: | ||
return "exec " + s.path | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters