Skip to content

Commit cc2aff7

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

File tree

19 files changed

+175
-38
lines changed

19 files changed

+175
-38
lines changed

devbox.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Devbox interface {
2121
// Adding duplicate packages is a no-op.
2222
Add(ctx context.Context, pkgs ...string) error
2323
Config() *devconfig.Config
24+
ExportHook(shellName string) (string, error)
2425
ProjectDir() string
2526
// Generate creates the directory of Nix files and the Dockerfile that define
2627
// the devbox environment.

internal/boxcli/export.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package boxcli
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// exportCmd is an alias of shellenv, but is also hidden and hence we cannot define it
10+
// simply using `Aliases: []string{"export"}` in the shellEnvCmd definition.
11+
func exportCmd() *cobra.Command {
12+
flags := shellEnvCmdFlags{}
13+
cmd := &cobra.Command{
14+
Use: "export [shell]",
15+
Hidden: true,
16+
Short: "Print shell command to setup the shell export to ensure an up-to-date environment",
17+
Args: cobra.ExactArgs(1),
18+
RunE: func(cmd *cobra.Command, args []string) error {
19+
s, err := shellEnvFunc(cmd, flags)
20+
if err != nil {
21+
return err
22+
}
23+
fmt.Fprintln(cmd.OutOrStdout(), s)
24+
return nil
25+
},
26+
}
27+
28+
registerShellEnvFlags(cmd, &flags)
29+
return cmd
30+
}
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
@@ -26,6 +26,7 @@ func globalCmd() *cobra.Command {
2626
}
2727

2828
addCommandAndHideConfigFlag(globalCmd, addCmd())
29+
addCommandAndHideConfigFlag(globalCmd, hookCmd())
2930
addCommandAndHideConfigFlag(globalCmd, installCmd())
3031
addCommandAndHideConfigFlag(globalCmd, pathCmd())
3132
addCommandAndHideConfigFlag(globalCmd, pullCmd())
@@ -112,7 +113,10 @@ func setGlobalConfigForDelegatedCommands(
112113
}
113114

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

internal/boxcli/hook.go

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

internal/boxcli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ func RootCmd() *cobra.Command {
5252
command.AddCommand(authCmd())
5353
}
5454
command.AddCommand(createCmd())
55+
command.AddCommand(exportCmd())
5556
command.AddCommand(generateCmd())
5657
command.AddCommand(globalCmd())
58+
command.AddCommand(hookCmd())
5759
command.AddCommand(infoCmd())
5860
command.AddCommand(initCmd())
5961
command.AddCommand(installCmd())

internal/boxcli/shellenv.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ func shellEnvCmd() *cobra.Command {
3636
},
3737
}
3838

39+
registerShellEnvFlags(command, &flags)
40+
command.AddCommand(shellEnvOnlyPathWithoutWrappersCmd())
41+
42+
return command
43+
}
44+
45+
func registerShellEnvFlags(command *cobra.Command, flags *shellEnvCmdFlags) {
46+
3947
command.Flags().BoolVar(
4048
&flags.runInitHook, "init-hook", false, "runs init hook after exporting shell environment")
4149
command.Flags().BoolVar(
@@ -45,10 +53,6 @@ func shellEnvCmd() *cobra.Command {
4553
&flags.pure, "pure", false, "If this flag is specified, devbox creates an isolated environment inheriting almost no variables from the current environment. A few variables, in particular HOME, USER and DISPLAY, are retained.")
4654

4755
flags.config.register(command)
48-
49-
command.AddCommand(shellEnvOnlyPathWithoutWrappersCmd())
50-
51-
return command
5256
}
5357

5458
func shellEnvFunc(cmd *cobra.Command, flags shellEnvCmdFlags) (string, error) {

internal/impl/devbox.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -822,21 +822,23 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
822822

823823
addEnvIfNotPreviouslySetByDevbox(env, pluginEnv)
824824

825+
envPaths := []string{}
826+
if !featureflag.PromptHook.Enabled() {
827+
envPaths = append(envPaths, filepath.Join(d.projectDir, plugin.WrapperBinPath))
828+
}
829+
// Adding profile bin path is a temporary hack. Some packages .e.g. curl
830+
// don't export the correct bin in the package, instead they export
831+
// as a propagated build input. This can be fixed in 2 ways:
832+
// * have NixBins() recursively look for bins in propagated build inputs
833+
// * Turn existing planners into flakes (i.e. php, haskell) and use the bins
834+
// in the profile.
835+
// Landau: I prefer option 2 because it doesn't require us to re-implement
836+
// nix recursive bin lookup.
837+
envPaths = append(envPaths, nix.ProfileBinPath(d.projectDir), env["PATH"])
838+
825839
// Prepend virtenv bin path first so user can override it if needed. Virtenv
826840
// is where the bin wrappers live
827-
env["PATH"] = JoinPathLists(
828-
filepath.Join(d.projectDir, plugin.WrapperBinPath),
829-
// Adding profile bin path is a temporary hack. Some packages .e.g. curl
830-
// don't export the correct bin in the package, instead they export
831-
// as a propagated build input. This can be fixed in 2 ways:
832-
// * have NixBins() recursively look for bins in propagated build inputs
833-
// * Turn existing planners into flakes (i.e. php, haskell) and use the bins
834-
// in the profile.
835-
// Landau: I prefer option 2 because it doesn't require us to re-implement
836-
// nix recursive bin lookup.
837-
nix.ProfileBinPath(d.projectDir),
838-
env["PATH"],
839-
)
841+
env["PATH"] = JoinPathLists(envPaths...)
840842

841843
// Include env variables in devbox.json
842844
configEnv := d.configEnvs(env)

internal/impl/generate/devcontainer_util.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"runtime/trace"
1818
"strings"
1919

20+
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2021
"go.jetpack.io/devbox/internal/debug"
2122
)
2223

@@ -159,5 +160,9 @@ func getDevcontainerContent(pkgs []string) *devcontainerObject {
159160
func EnvrcContent(w io.Writer) error {
160161
tmplName := "envrcContent.tmpl"
161162
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
162-
return t.Execute(w, nil)
163+
return t.Execute(w, struct {
164+
PromptHookEnabled bool
165+
}{
166+
PromptHookEnabled: featureflag.PromptHook.Enabled(),
167+
})
163168
}
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/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)
@@ -411,3 +417,23 @@ func filterPathList(pathList string, keep func(string) bool) string {
411417
}
412418
return strings.Join(filtered, string(filepath.ListSeparator))
413419
}
420+
421+
func (d *Devbox) ExportHook(shellName string) (string, error) {
422+
if !featureflag.PromptHook.Enabled() {
423+
return "", nil
424+
}
425+
426+
// TODO: use a single common "enum" for both shenv and DevboxShell
427+
hookTemplate, err := shenv.DetectShell(shellName).Hook()
428+
if err != nil {
429+
return "", err
430+
}
431+
432+
var buf bytes.Buffer
433+
err = template.Must(template.New("hookTemplate").Parse(hookTemplate)).
434+
Execute(&buf, struct{ ProjectDir string }{ProjectDir: d.projectDir})
435+
if err != nil {
436+
return "", errors.WithStack(err)
437+
}
438+
return buf.String(), nil
439+
}

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+
{{ if .PromptHookEnabled }}
72+
# Ensure devbox shellenv is evaluated
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+
{{ if .PromptHookEnabled }}
75+
# Ensure devbox shellenv is evaluated
76+
devbox hook fish -c "{{ .ProjectDir }}" | source
77+
{{ end }}

internal/impl/testdata/shellrc/basic/shellrc.golden

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ if ! type refresh >/dev/null 2>&1; then
2727
alias refresh='eval $(devbox shellenv)'
2828
export DEVBOX_REFRESH_ALIAS="refresh"
2929
fi
30+
31+

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)