Skip to content

Commit 26f5204

Browse files
authored
shell,nix: set original PATH at start of init file (#57)
This fixes a bug that causes the devbox shell's init to fail when the user's rcfile tries to call a command outside of devbox. For example, the following line in a ~/.bashrc will trigger an error if the devbox doesn't have Go installed: export PATH="$(go env GOPATH):$PATH" This is because we're adding the ORIGINAL_PATH at the end of their shell init script, which means that anything before that only see Nix packages. Fix this by adding a pre-init hook that lets us restore the PATH before the user's rcfile, while still forcing Nix packages to come first in the post-init hook. --- In the process of testing and debugging this PR, I added a whole bunch of debug logging. It should probably be a separate commit, but I'm hesitant to touch anything now that things are working.
1 parent 7ae5af0 commit 26f5204

File tree

7 files changed

+162
-42
lines changed

7 files changed

+162
-42
lines changed

boxcli/midcobra/debug.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
package midcobra
55

66
import (
7-
"log"
87
"os"
8+
"strconv"
99

1010
"github.com/spf13/cobra"
1111
"github.com/spf13/pflag"
12+
"go.jetpack.io/devbox/debug"
1213
)
1314

1415
type DebugMiddleware struct {
@@ -27,17 +28,25 @@ func (d *DebugMiddleware) AttachToFlag(flags *pflag.FlagSet, flagName string) {
2728
d.flag.Hidden = true
2829
}
2930

30-
func (d *DebugMiddleware) preRun(cmd *cobra.Command, args []string) {}
31+
func (d *DebugMiddleware) preRun(cmd *cobra.Command, args []string) {
32+
if d == nil {
33+
return
34+
}
3135

32-
func (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
33-
if runErr != nil && d.Debug() {
34-
log.Printf("Error: %+v\n", runErr)
36+
strVal := ""
37+
if d.flag.Changed {
38+
strVal = d.flag.Value.String()
39+
} else {
40+
strVal = os.Getenv("DEVBOX_DEBUG")
41+
}
42+
if enabled, _ := strconv.ParseBool(strVal); enabled {
43+
debug.Enable()
3544
}
3645
}
3746

38-
func (d *DebugMiddleware) Debug() bool {
39-
if d != nil && d.flag.Changed {
40-
return d.flag.Value.String() == "true"
47+
func (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
48+
if runErr == nil {
49+
return
4150
}
42-
return os.Getenv("DEBUG") != ""
51+
debug.Log("Error: %+v\n", runErr)
4352
}

boxcli/root.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ package boxcli
66
import (
77
"context"
88
"errors"
9-
"fmt"
109
"os"
1110
"os/exec"
1211

1312
"github.com/spf13/cobra"
1413
"go.jetpack.io/devbox/boxcli/midcobra"
1514
"go.jetpack.io/devbox/build"
15+
"go.jetpack.io/devbox/debug"
1616
)
1717

1818
var debugMiddleware *midcobra.DebugMiddleware = &midcobra.DebugMiddleware{}
@@ -51,15 +51,7 @@ func RootCmd() *cobra.Command {
5151
}
5252

5353
func Execute(ctx context.Context, args []string) int {
54-
defer func() {
55-
if r := recover(); r != nil {
56-
if debugMiddleware.Debug() {
57-
fmt.Printf("PANIC (DEBUG MODE ON): %+v\n", r)
58-
} else {
59-
fmt.Printf("Error: %s\n", r)
60-
}
61-
}
62-
}()
54+
defer debug.Recover()
6355
exe := midcobra.New(RootCmd())
6456
exe.AddMiddleware(midcobra.Telemetry(&midcobra.TelemetryOpts{
6557
AppName: "devbox",

debug/debug.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package debug
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
"strconv"
8+
)
9+
10+
var enabled bool
11+
12+
func init() {
13+
enabled, _ = strconv.ParseBool(os.Getenv("DEBUG"))
14+
}
15+
16+
func IsEnabled() bool { return enabled }
17+
18+
func Enable() {
19+
enabled = true
20+
log.SetPrefix("[DEBUG] ")
21+
log.SetFlags(log.Llongfile | log.Ldate | log.Ltime)
22+
_ = log.Output(2, "Debug mode enabled.")
23+
}
24+
25+
func Log(format string, v ...any) {
26+
if !enabled {
27+
return
28+
}
29+
_ = log.Output(2, fmt.Sprintf(format, v...))
30+
}
31+
32+
func Recover() {
33+
r := recover()
34+
if r == nil {
35+
return
36+
}
37+
38+
if enabled {
39+
log.Println("Allowing panic because debug mode is enabled.")
40+
panic(r)
41+
}
42+
fmt.Println("Error:", r)
43+
}

generate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"text/template"
1414

1515
"github.com/pkg/errors"
16+
"go.jetpack.io/devbox/debug"
1617
"go.jetpack.io/devbox/planner"
1718
)
1819

@@ -67,4 +68,5 @@ func toJSON(a any) string {
6768
var templateFuncs = template.FuncMap{
6869
"json": toJSON,
6970
"contains": strings.Contains,
71+
"debug": debug.IsEnabled,
7072
}

nix/nix.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"os/exec"
1212
"strings"
1313

14+
"go.jetpack.io/devbox/debug"
1415
"go.jetpack.io/devbox/shell"
1516
)
1617

@@ -44,20 +45,32 @@ func Shell(path string) error {
4445
//
4546
// ORIGINAL_PATH is set by sh.StartCommand.
4647
// PURE_NIX_PATH is set by the shell hook in shell.nix.tmpl.
47-
_ = sh.SetInit(`
48-
# Update the $PATH so the user can keep using programs that live outside of Nix,
49-
# but prefer anything installed by Nix.
48+
sh.PreInitHook = `
49+
# Update the $PATH so that the user's init script has access to all of their
50+
# non-devbox programs.
51+
export PATH="$PURE_NIX_PATH:$ORIGINAL_PATH"
52+
`
53+
sh.PostInitHook = `
54+
# Update the $PATH again so that the Nix packages take priority over the
55+
# programs outside of devbox.
5056
export PATH="$PURE_NIX_PATH:$ORIGINAL_PATH"
5157
5258
# Prepend to the prompt to make it clear we're in a devbox shell.
5359
export PS1="(devbox) $PS1"
54-
`)
60+
`
61+
62+
if debug.IsEnabled() {
63+
sh.PostInitHook += `echo "POST-INIT PATH=$PATH"
64+
`
65+
}
5566

5667
cmd := exec.Command("nix-shell", path)
5768
cmd.Args = append(cmd.Args, "--pure", "--command", sh.ExecCommand())
5869
cmd.Stdin = os.Stdin
5970
cmd.Stdout = os.Stdout
6071
cmd.Stderr = os.Stderr
72+
73+
debug.Log("Executing nix-shell command: %v", cmd.Args)
6174
return cmd.Run()
6275
}
6376

@@ -66,6 +79,8 @@ func runFallbackShell(path string) error {
6679
cmd.Stdin = os.Stdin
6780
cmd.Stdout = os.Stdout
6881
cmd.Stderr = os.Stderr
82+
83+
debug.Log("Unrecognized user shell, falling back to: %v", cmd.Args)
6984
return cmd.Run()
7085
}
7186

shell/shell.go

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"os"
1313
"path/filepath"
1414
"strings"
15+
16+
"go.jetpack.io/devbox/debug"
1517
)
1618

1719
type name string
@@ -30,6 +32,22 @@ type Shell struct {
3032
path string
3133
initFile string
3234
devboxInitFile string
35+
36+
// PreInitHook contains commands that will run before the user's init
37+
// files at shell startup.
38+
//
39+
// The script's environment will contain an ORIGINAL_PATH environment
40+
// variable, which will bet set to the PATH before the shell's init
41+
// files have had a chance to modify it.
42+
PreInitHook string
43+
44+
// PostInitHook contains commands that will run after the user's init
45+
// files at shell startup.
46+
//
47+
// The script's environment will contain an ORIGINAL_PATH environment
48+
// variable, which will bet set to the PATH before the shell's init
49+
// files have had a chance to modify it.
50+
PostInitHook string
3351
}
3452

3553
// Detect attempts to determine the user's default shell.
@@ -67,6 +85,9 @@ func Detect() (*Shell, error) {
6785
default:
6886
sh.name = shUnknown
6987
}
88+
debug.Log("Detected shell: %s", sh.path)
89+
debug.Log("Recognized shell as: %s", sh.path)
90+
debug.Log("Looking for user's shell init file at: %s", sh.initFile)
7091
return sh, nil
7192
}
7293

@@ -80,33 +101,62 @@ func rcfilePath(basename string) string {
80101
return filepath.Join(home, basename)
81102
}
82103

83-
// SetInit configures the shell to run a script at startup. The script runs
84-
// after the user's usual init files. The script's environment will contain an
85-
// ORIGINAL_PATH environment variable, which will bet set to the PATH before
86-
// the user's init files have had a chance to modify it.
87-
func (s *Shell) SetInit(script string) error {
88-
script = strings.TrimSpace(script)
89-
if script == "" {
90-
return nil
104+
func (s *Shell) buildInitFile() ([]byte, error) {
105+
prehook := strings.TrimSpace(s.PreInitHook)
106+
posthook := strings.TrimSpace(s.PostInitHook)
107+
if prehook == "" && posthook == "" {
108+
return nil, nil
109+
}
110+
111+
buf := bytes.Buffer{}
112+
if prehook != "" {
113+
buf.WriteString(`
114+
# Begin Devbox Pre-init Hook
115+
116+
`)
117+
buf.WriteString(prehook)
118+
buf.WriteString(`
119+
120+
# End Devbox Pre-init Hook
121+
122+
`)
91123
}
92124

93-
initFile, _ := os.ReadFile(s.initFile)
125+
initFile, err := os.ReadFile(s.initFile)
126+
if err != nil {
127+
return nil, err
128+
}
94129
initFile = bytes.TrimSpace(initFile)
95130
if len(initFile) > 0 {
96-
initFile = append(initFile, '\n', '\n')
131+
buf.Write(initFile)
97132
}
98133

99-
buf := bytes.NewBuffer(initFile)
100-
buf.WriteString(`
134+
if posthook != "" {
135+
buf.WriteString(`
101136
102-
# Begin Devbox Shell Hook
137+
# Begin Devbox Pre-init Hook
103138
104139
`)
105-
buf.WriteString(script)
106-
buf.WriteString(`
140+
buf.WriteString(posthook)
141+
buf.WriteString(`
107142
108-
# End Devbox Shell Hook
109-
`)
143+
# End Devbox Post-init Hook`)
144+
}
145+
146+
b := buf.Bytes()
147+
b = bytes.TrimSpace(b)
148+
if len(b) == 0 {
149+
return nil, nil
150+
}
151+
b = append(b, '\n')
152+
return b, nil
153+
}
154+
155+
func (s *Shell) writeHooks() error {
156+
initContents, err := s.buildInitFile()
157+
if err != nil {
158+
return err
159+
}
110160

111161
// We need a temp dir (as opposed to a temp file) because zsh uses
112162
// ZDOTDIR to point to a new directory containing the .zshrc.
@@ -115,16 +165,20 @@ func (s *Shell) SetInit(script string) error {
115165
return fmt.Errorf("create temp dir for shell init file: %v", err)
116166
}
117167
devboxInitFile := filepath.Join(tmp, filepath.Base(s.initFile))
118-
if err := os.WriteFile(devboxInitFile, buf.Bytes(), 0600); err != nil {
168+
if err := os.WriteFile(devboxInitFile, initContents, 0600); err != nil {
119169
return fmt.Errorf("write to shell init file: %v", err)
120170
}
121171
s.devboxInitFile = devboxInitFile
172+
173+
debug.Log("Wrote devbox shell init file to: %s", s.devboxInitFile)
174+
debug.Log("--- Begin Devbox Shell Init Contents ---\n%s--- End Devbox Shell Init Contents ---", initContents)
122175
return nil
123176
}
124177

125178
// ExecCommand is a command that replaces the current shell with s.
126179
func (s *Shell) ExecCommand() string {
127-
if s.devboxInitFile == "" {
180+
if err := s.writeHooks(); err != nil || s.devboxInitFile == "" {
181+
debug.Log("Failed to write shell pre-init and post-init hooks: %v", err)
128182
return "exec " + s.path
129183
}
130184

tmpl/shell.nix.tmpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ mkShell {
2121
# Make sure we include a basic path for the devbox shell. Otherwise it may
2222
# fail to start.
2323
export PATH=$PATH:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
24+
25+
{{ if debug }}
26+
echo "PURE_NIX_PATH=$PURE_NIX_PATH"
27+
echo "PATH=$PATH"
28+
{{- end }}
2429
'';
2530
packages = [
2631
{{- range .Packages}}

0 commit comments

Comments
 (0)