Skip to content

Commit 9e3638f

Browse files
authored
[RFC][direnv-inspired] introduce export and hook commands, use in devbox shell, global and direnv (#1172)
## Summary See #1156 for overall motivation. **In this PR:** 1. Turn off adding bin-wrappers path to $PATH from `devbox shellenv`. 2. We add commands `devbox hook` and `devbox export`, analogous to `direnv hook` and `direnv export`. - `export` is an alias of shellenv, and is hidden - `hook` registers a shell prompt hook that wil call `eval $(devbox export)` 3. During `devbox shell`, we add `eval $(devbox hook)` to the generated shellrc file. 4. During `direnv`, the code is essentially unchanged except calling`devbox export` instead of `devbox shellenv`. 5. For global, the users are currently expected to add `eval $(devbox global hook <shell>)` **RFC proposal to roll out:** 1. I don't think we need `devbox export`. We can continue with `devbox shellenv`. 2. We can remove `devbox hook` entirely: in the generated shellrc, we can inline the code of the hook that calls `devbox export`. 3. We can introduce `devbox global activate` to have the functionality that `devbox global hook` has in this PR. - `activate` is a generic word, enabling us to change its functionality under-the-hood. It would save us the dilemma we have with this PR about how to make users change their shellrc code from `devbox global shellenv` to `devbox global hook`. - We can make `devbox global shellenv` an alias of `devbox global activate`. This would save users from needing to update their personal shellrc files. ## How was it tested? Added a feature flag: `DEVBOX_FEATURE_PROMPT_HOOK=1`. Did some basic testing: - project: - `devbox shell` and then edited `devbox.json` to add a new package, and saw the new package being installed prior to next prompt being displayed. - `devbox run -- iex -S mix` worked as expected, since we can turn off the bin-wrappers. - global: - added `devbox global hook | source` to my fish shell. - `devbox global ls` worked. - `devbox global add <package>` worked as before. **Note:** this PR adds the basic functionality, but the feature is not yet robust. For a full list of pending scenarios that must work, please refer to the (internal) notion doc.
1 parent fe1db1f commit 9e3638f

18 files changed

+173
-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

0 commit comments

Comments
 (0)