Skip to content

Commit a9c13a8

Browse files
committed
[direnv-inspired] add export and hook commands, and hook up shell, global and direnv
1 parent e24ad15 commit a9c13a8

File tree

16 files changed

+133
-35
lines changed

16 files changed

+133
-35
lines changed

devbox.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Devbox interface {
2222
// Adding duplicate packages is a no-op.
2323
Add(ctx context.Context, pkgs ...string) error
2424
Config() *devconfig.Config
25+
ExportHook(shellName string) (string, error)
2526
ProjectDir() string
2627
// Generate creates the directory of Nix files and the Dockerfile that define
2728
// the devbox environment.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package featureflag
2+
3+
// PromptHook controls the insertion of a shell prompt hook that invokes
4+
// devbox shellenv, in lieu of using binary wrappers.
5+
var PromptHook = disabled("PROMPT_HOOK")

internal/boxcli/global.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func globalCmd() *cobra.Command {
2525
}
2626

2727
addCommandAndHideConfigFlag(globalCmd, addCmd())
28+
addCommandAndHideConfigFlag(globalCmd, hookCmd())
2829
addCommandAndHideConfigFlag(globalCmd, installCmd())
2930
addCommandAndHideConfigFlag(globalCmd, pathCmd())
3031
addCommandAndHideConfigFlag(globalCmd, pullCmd())
@@ -111,7 +112,10 @@ func setGlobalConfigForDelegatedCommands(
111112
}
112113

113114
func ensureGlobalEnvEnabled(cmd *cobra.Command, args []string) error {
114-
if cmd.Name() == "shellenv" {
115+
// Skip checking this for shellenv and hook sub-commands of devbox global
116+
// since these commands are what will enable the global environment when
117+
// invoked from the user's shellrc
118+
if cmd.Name() == "shellenv" || cmd.Name() == "hook" {
115119
return nil
116120
}
117121
path, err := ensureGlobalConfig(cmd)

internal/boxcli/hook.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package boxcli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
"go.jetpack.io/devbox"
8+
)
9+
10+
type hookFlags struct {
11+
config configFlags
12+
}
13+
14+
func hookCmd() *cobra.Command {
15+
flags := hookFlags{}
16+
cmd := &cobra.Command{
17+
Use: "hook [shell]",
18+
Short: "Print shell command to setup the shell hook to ensure an up-to-date environment",
19+
Args: cobra.ExactArgs(1),
20+
RunE: func(cmd *cobra.Command, args []string) error {
21+
output, err := hookFunc(cmd, args, flags)
22+
if err != nil {
23+
return err
24+
}
25+
fmt.Fprintf(cmd.OutOrStdout(), output)
26+
return nil
27+
},
28+
}
29+
30+
flags.config.register(cmd)
31+
return cmd
32+
}
33+
34+
func hookFunc(cmd *cobra.Command, args []string, flags hookFlags) (string, error) {
35+
box, err := devbox.Open(flags.config.path, cmd.ErrOrStderr())
36+
if err != nil {
37+
return "", err
38+
}
39+
return box.ExportHook(args[0])
40+
}

internal/boxcli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func RootCmd() *cobra.Command {
5454
command.AddCommand(createCmd())
5555
command.AddCommand(generateCmd())
5656
command.AddCommand(globalCmd())
57+
command.AddCommand(hookCmd())
5758
command.AddCommand(infoCmd())
5859
command.AddCommand(initCmd())
5960
command.AddCommand(installCmd())

internal/boxcli/shellenv.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ func shellEnvCmd() *cobra.Command {
2222
flags := shellEnvCmdFlags{}
2323
command := &cobra.Command{
2424
Use: "shellenv",
25+
Aliases: []string{"export"},
2526
Short: "Print shell commands that add Devbox packages to your PATH",
2627
Args: cobra.ExactArgs(0),
2728
PreRunE: ensureNixInstalled,

internal/impl/devbox.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,11 @@ func (d *Devbox) GenerateDockerfile(force bool) error {
441441
func PrintEnvrcContent(w io.Writer) error {
442442
tmplName := "envrcContent.tmpl"
443443
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
444-
// write content into file
445-
return t.Execute(w, nil)
444+
return t.Execute(w, struct {
445+
PromptHookEnabled bool
446+
}{
447+
PromptHookEnabled: featureflag.PromptHook.Enabled(),
448+
})
446449
}
447450

448451
// GenerateEnvrcFile generates a .envrc file that makes direnv integration convenient
@@ -839,21 +842,23 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
839842

840843
addEnvIfNotPreviouslySetByDevbox(env, pluginEnv)
841844

845+
envPaths := []string{}
846+
if !featureflag.PromptHook.Enabled() {
847+
envPaths = append(envPaths, filepath.Join(d.projectDir, plugin.WrapperBinPath))
848+
}
849+
// Adding profile bin path is a temporary hack. Some packages .e.g. curl
850+
// don't export the correct bin in the package, instead they export
851+
// as a propagated build input. This can be fixed in 2 ways:
852+
// * have NixBins() recursively look for bins in propagated build inputs
853+
// * Turn existing planners into flakes (i.e. php, haskell) and use the bins
854+
// in the profile.
855+
// Landau: I prefer option 2 because it doesn't require us to re-implement
856+
// nix recursive bin lookup.
857+
envPaths = append(envPaths, nix.ProfileBinPath(d.projectDir), env["PATH"])
858+
842859
// Prepend virtenv bin path first so user can override it if needed. Virtenv
843860
// is where the bin wrappers live
844-
env["PATH"] = JoinPathLists(
845-
filepath.Join(d.projectDir, plugin.WrapperBinPath),
846-
// Adding profile bin path is a temporary hack. Some packages .e.g. curl
847-
// don't export the correct bin in the package, instead they export
848-
// as a propagated build input. This can be fixed in 2 ways:
849-
// * have NixBins() recursively look for bins in propagated build inputs
850-
// * Turn existing planners into flakes (i.e. php, haskell) and use the bins
851-
// in the profile.
852-
// Landau: I prefer option 2 because it doesn't require us to re-implement
853-
// nix recursive bin lookup.
854-
nix.ProfileBinPath(d.projectDir),
855-
env["PATH"],
856-
)
861+
env["PATH"] = JoinPathLists(envPaths...)
857862

858863
// Include env variables in devbox.json
859864
configEnv := d.configEnvs(env)

internal/impl/shell.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616

1717
"github.com/alessio/shellescape"
1818
"github.com/pkg/errors"
19+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
20+
"go.jetpack.io/devbox/internal/shenv"
1921

2022
"go.jetpack.io/devbox/internal/debug"
2123
"go.jetpack.io/devbox/internal/envir"
@@ -314,21 +316,25 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
314316
}
315317

316318
err = tmpl.Execute(shellrcf, struct {
317-
ProjectDir string
318-
OriginalInit string
319-
OriginalInitPath string
320-
HooksFilePath string
321-
ShellStartTime string
322-
HistoryFile string
323-
ExportEnv string
319+
ProjectDir string
320+
OriginalInit string
321+
OriginalInitPath string
322+
HooksFilePath string
323+
ShellName string
324+
ShellStartTime string
325+
HistoryFile string
326+
ExportEnv string
327+
PromptHookEnabled bool
324328
}{
325-
ProjectDir: s.projectDir,
326-
OriginalInit: string(bytes.TrimSpace(userShellrc)),
327-
OriginalInitPath: s.userShellrcPath,
328-
HooksFilePath: s.hooksFilePath,
329-
ShellStartTime: s.shellStartTime,
330-
HistoryFile: strings.TrimSpace(s.historyFile),
331-
ExportEnv: exportify(s.env),
329+
ProjectDir: s.projectDir,
330+
OriginalInit: string(bytes.TrimSpace(userShellrc)),
331+
OriginalInitPath: s.userShellrcPath,
332+
HooksFilePath: s.hooksFilePath,
333+
ShellName: string(s.name),
334+
ShellStartTime: s.shellStartTime,
335+
HistoryFile: strings.TrimSpace(s.historyFile),
336+
ExportEnv: exportify(s.env),
337+
PromptHookEnabled: featureflag.PromptHook.Enabled(),
332338
})
333339
if err != nil {
334340
return "", fmt.Errorf("execute shellrc template: %v", err)
@@ -429,3 +435,23 @@ func findNixInPATH(env map[string]string) (string, error) {
429435
// did not find nix executable in PATH, return error
430436
return "", errors.New("could not find any nix executable in PATH. Make sure Nix is installed and in PATH, then try again")
431437
}
438+
439+
func (d *Devbox) ExportHook(shellName string) (string, error) {
440+
if !featureflag.PromptHook.Enabled() {
441+
return "", nil
442+
}
443+
444+
// TODO: use a single common "enum" for both shenv and DevboxShell
445+
hookTemplate, err := shenv.DetectShell(shellName).Hook()
446+
if err != nil {
447+
return "", err
448+
}
449+
450+
var buf bytes.Buffer
451+
err = template.Must(template.New("hookTemplate").Parse(hookTemplate)).
452+
Execute(&buf, struct{ ProjectDir string }{ProjectDir: d.projectDir})
453+
if err != nil {
454+
return "", errors.WithStack(err)
455+
}
456+
return buf.String(), nil
457+
}

internal/impl/shellrc.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,9 @@ if ! type refresh >/dev/null 2>&1; then
6767
alias refresh='eval $(devbox shellenv)'
6868
export DEVBOX_REFRESH_ALIAS="refresh"
6969
fi
70+
71+
# Ensure devbox shellenv is evaluated
72+
{{ if .PromptHookEnabled }}
73+
# TODO savil: how do I wrap ProjectDir in quotes?
74+
eval "$(devbox hook {{ .ShellName }} -c {{ .ProjectDir }})"
75+
{{ end }}

internal/impl/shellrc_fish.tmpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,8 @@ if not type refresh >/dev/null 2>&1
7070
alias refresh='eval (devbox shellenv | string collect)'
7171
export DEVBOX_REFRESH_ALIAS="refresh"
7272
end
73+
74+
# Ensure devbox shellenv is evaluated
75+
{{ if .PromptHookEnabled }}
76+
devbox hook fish -c "{{ .ProjectDir }}" | source
77+
{{ end }}

internal/impl/tmpl/envrcContent.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
use_devbox() {
22
watch_file devbox.json
3+
{{ .PromptHookEnabled }}
4+
eval "$(devbox export --init-hook --install)"
5+
{{ else }}
36
eval "$(devbox shellenv --init-hook --install)"
7+
{{ end }}
48
}
59
use devbox

internal/shenv/shell_bash.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const bashHook = `
1111
_devbox_hook() {
1212
local previous_exit_status=$?;
1313
trap -- '' SIGINT;
14-
eval "$(devbox shellenv --config {{ .ProjectDir }})";
14+
eval "$(devbox export --config {{ .ProjectDir }})";
1515
trap - SIGINT;
1616
return $previous_exit_status;
1717
};

internal/shenv/shell_fish.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var Fish Shell = fish{}
1212

1313
const fishHook = `
1414
function __devbox_shellenv_eval --on-event fish_prompt;
15-
devbox shellenv --config {{ .ProjectDir }} | source;
15+
devbox export --config {{ .ProjectDir }} | source;
1616
end;
1717
`
1818

internal/shenv/shell_ksh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ var Ksh Shell = ksh{}
88
// um, this is ChatGPT writing it. I need to verify and test
99
const kshHook = `
1010
_devbox_hook() {
11-
eval "$(devbox shellenv --config {{ .ProjectDir }})";
11+
eval "$(devbox export --config {{ .ProjectDir }})";
1212
}
1313
if [[ "$(typeset -f precmd)" != *"_devbox_hook"* ]]; then
1414
function precmd {

internal/shenv/shell_posix.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const posixHook = `
1212
_devbox_hook() {
1313
local previous_exit_status=$?
1414
trap : INT
15-
eval "$(devbox shellenv --config {{ .ProjectDir }})"
15+
eval "$(devbox export --config {{ .ProjectDir }})"
1616
trap - INT
1717
return $previous_exit_status
1818
}

internal/shenv/shell_zsh.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var Zsh Shell = zsh{}
99
const zshHook = `
1010
_devbox_hook() {
1111
trap -- '' SIGINT;
12-
eval "$(devbox shellenv --config {{ .ProjectDir }})";
12+
eval "$(devbox export --config {{ .ProjectDir }})";
1313
trap - SIGINT;
1414
}
1515
typeset -ag precmd_functions;

0 commit comments

Comments
 (0)