Skip to content

Commit 95ab2db

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

File tree

17 files changed

+138
-62
lines changed

17 files changed

+138
-62
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.
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func globalCmd() *cobra.Command {
2424
}
2525

2626
addCommandAndHideConfigFlag(globalCmd, addCmd())
27+
addCommandAndHideConfigFlag(globalCmd, hookCmd())
2728
addCommandAndHideConfigFlag(globalCmd, installCmd())
2829
addCommandAndHideConfigFlag(globalCmd, pathCmd())
2930
addCommandAndHideConfigFlag(globalCmd, pullCmd())
@@ -107,7 +108,7 @@ func setGlobalConfigForDelegatedCommands(
107108
}
108109

109110
func ensureGlobalEnvEnabled(cmd *cobra.Command, args []string) error {
110-
if cmd.Name() == "shellenv" {
111+
if cmd.Name() == "shellenv" || cmd.Name() == "hook" {
111112
return nil
112113
}
113114
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
@@ -50,6 +50,7 @@ func RootCmd() *cobra.Command {
5050
command.AddCommand(createCmd())
5151
command.AddCommand(generateCmd())
5252
command.AddCommand(globalCmd())
53+
command.AddCommand(hookCmd())
5354
command.AddCommand(infoCmd())
5455
command.AddCommand(initCmd())
5556
command.AddCommand(installCmd())

internal/boxcli/shellenv.go

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

internal/impl/devbox.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,11 @@ func (d *Devbox) GenerateDockerfile(force bool) error {
412412
func PrintEnvrcContent(w io.Writer) error {
413413
tmplName := "envrcContent.tmpl"
414414
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
415-
// write content into file
416-
return t.Execute(w, nil)
415+
return t.Execute(w, struct {
416+
PromptHookEnabled bool
417+
}{
418+
PromptHookEnabled: featureflag.PromptHook.Enabled(),
419+
})
417420
}
418421

419422
// GenerateEnvrcFile generates a .envrc file that makes direnv integration convenient
@@ -816,21 +819,23 @@ func (d *Devbox) computeNixEnv(ctx context.Context, usePrintDevEnvCache bool) (m
816819

817820
addEnvIfNotPreviouslySetByDevbox(env, pluginEnv)
818821

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

835840
// Include env variables in devbox.json
836841
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"
@@ -312,21 +314,25 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
312314
}
313315

314316
err = tmpl.Execute(shellrcf, struct {
315-
ProjectDir string
316-
OriginalInit string
317-
OriginalInitPath string
318-
HooksFilePath string
319-
ShellStartTime string
320-
HistoryFile string
321-
ExportEnv string
317+
ProjectDir string
318+
OriginalInit string
319+
OriginalInitPath string
320+
HooksFilePath string
321+
ShellName string
322+
ShellStartTime string
323+
HistoryFile string
324+
ExportEnv string
325+
PromptHookEnabled bool
322326
}{
323-
ProjectDir: s.projectDir,
324-
OriginalInit: string(bytes.TrimSpace(userShellrc)),
325-
OriginalInitPath: s.userShellrcPath,
326-
HooksFilePath: s.hooksFilePath,
327-
ShellStartTime: s.shellStartTime,
328-
HistoryFile: strings.TrimSpace(s.historyFile),
329-
ExportEnv: exportify(s.env),
327+
ProjectDir: s.projectDir,
328+
OriginalInit: string(bytes.TrimSpace(userShellrc)),
329+
OriginalInitPath: s.userShellrcPath,
330+
HooksFilePath: s.hooksFilePath,
331+
ShellName: string(s.name),
332+
ShellStartTime: s.shellStartTime,
333+
HistoryFile: strings.TrimSpace(s.historyFile),
334+
ExportEnv: exportify(s.env),
335+
PromptHookEnabled: featureflag.PromptHook.Enabled(),
330336
})
331337
if err != nil {
332338
return "", fmt.Errorf("execute shellrc template: %v", err)
@@ -399,3 +405,23 @@ func JoinPathLists(pathLists ...string) string {
399405
}
400406
return strings.Join(cleaned, string(filepath.ListSeparator))
401407
}
408+
409+
func (d *Devbox) ExportHook(shellName string) (string, error) {
410+
if !featureflag.PromptHook.Enabled() {
411+
return "", nil
412+
}
413+
414+
// TODO: use a single common "enum" for both shenv and DevboxShell
415+
hookTemplate, err := shenv.DetectShell(shellName).Hook()
416+
if err != nil {
417+
return "", err
418+
}
419+
420+
var buf bytes.Buffer
421+
err = template.Must(template.New("hookTemplate").Parse(hookTemplate)).
422+
Execute(&buf, struct{ ProjectDir string }{ProjectDir: d.projectDir})
423+
if err != nil {
424+
return "", errors.WithStack(err)
425+
}
426+
return buf.String(), nil
427+
}

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;

internal/shenv/shenv.go

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package shenv
22

3-
import (
4-
"path/filepath"
5-
)
6-
73
type Env map[string]string
84

95
// Shell is the interface that represents the interaction with the host shell.
@@ -33,36 +29,21 @@ func (e ShellExport) Remove(key string) {
3329
e[key] = nil
3430
}
3531

36-
// DetectShell returns a Shell instance from the given target.
37-
//
38-
// target is usually $0 and can also be prefixed by `-`
32+
// DetectShell returns a Shell instance from the given shell name
33+
// TODO: use a single common "enum" for both shenv and DevboxShell
3934
func DetectShell(target string) Shell {
40-
target = filepath.Base(target)
41-
// $0 starts with "-"
42-
if target[0:1] == "-" {
43-
target = target[1:]
44-
}
45-
4635
switch target {
4736
case "bash":
4837
return Bash
49-
//case "elvish":
50-
// return Elvish
5138
case "fish":
5239
return Fish
53-
//case "gha":
54-
// return GitHubActions
55-
//case "gzenv":
56-
// return GzEnv
57-
//case "json":
58-
// return JSON
59-
//case "tcsh":
60-
// return Tcsh
61-
//case "vim":
62-
// return Vim
40+
case "ksh":
41+
return Ksh
42+
case "posix":
43+
return Posix
6344
case "zsh":
6445
return Zsh
46+
default:
47+
return UnknownSh
6548
}
66-
67-
return nil
6849
}

0 commit comments

Comments
 (0)