Skip to content

Commit f520cc5

Browse files
mikeland73gcurtis
andauthored
[nix-installer] auto-install nix if we detect it's missing (#343)
## Summary This change makes devbox attempt to install nix if it is missing and the command the user ran requires it. It shows yellow text alerting the user before it starts (otherwise it is really jarring). One improvement could be to combine the sudo warning into the yellow text. If the installation fails we show the error that caused it. If the installation succeeds we show a user warning which is yellow text. This warning is an error which causes the CLI execution to end. We need this because nix requires restarting the terminal. This is another area where having a pkg installer may be an improvement. cc: @Lagoja ## How was it tested? Ran `devbox add mariadb` without having nix installed. Got: <img width="471" alt="image" src="https://user-images.githubusercontent.com/544948/205784786-c76d90f2-9846-418e-93db-d2605eddfc32.png"> Pressing enter started the installation. After installation we show: <img width="741" alt="image" src="https://user-images.githubusercontent.com/544948/205785207-da291c1b-2c5f-4004-9bb5-a3a3e053e94c.png"> Co-authored-by: Greg Curtis <greg.curtis@jetpack.io>
1 parent 21c3ae6 commit f520cc5

File tree

8 files changed

+90
-13
lines changed

8 files changed

+90
-13
lines changed

boxcli/add.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/pkg/errors"
1010
"github.com/spf13/cobra"
1111
"go.jetpack.io/devbox"
12+
"go.jetpack.io/devbox/nix"
1213
)
1314

1415
type addCmdFlags struct {
@@ -22,7 +23,7 @@ func AddCmd() *cobra.Command {
2223
Use: "add <pkg>...",
2324
Short: "Add a new package to your devbox",
2425
Args: cobra.MinimumNArgs(1),
25-
PersistentPreRunE: nixShellPersistentPreRunE,
26+
PersistentPreRunE: nix.EnsureInstalled,
2627
RunE: func(cmd *cobra.Command, args []string) error {
2728
return addCmdFunc(cmd, args, flags)
2829
},

boxcli/featureflag/nixinstaller.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package featureflag
2+
3+
var NixInstaller = disabled("NIX_INSTALLER") // DEVBOX_FEATURE_NIX_INSTALLER

boxcli/info.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/spf13/cobra"
1111
"go.jetpack.io/devbox"
1212
"go.jetpack.io/devbox/boxcli/featureflag"
13+
"go.jetpack.io/devbox/nix"
1314
)
1415

1516
type infoCmdFlags struct {
@@ -24,7 +25,7 @@ func InfoCmd() *cobra.Command {
2425
Hidden: !featureflag.PKGConfig.Enabled(),
2526
Short: "Display package info",
2627
Args: cobra.ExactArgs(1),
27-
PersistentPreRunE: nixShellPersistentPreRunE,
28+
PersistentPreRunE: nix.EnsureInstalled,
2829
RunE: func(cmd *cobra.Command, args []string) error {
2930
return infoCmdFunc(cmd, args[0], flags)
3031
},

boxcli/midcobra/debug.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ func (d *DebugMiddleware) postRun(cmd *cobra.Command, args []string, runErr erro
5252
return
5353
}
5454
if usererr.HasUserMessage(runErr) {
55-
color.Red("\nError: " + runErr.Error() + "\n\n")
55+
if usererr.IsWarning(runErr) {
56+
color.Yellow("\nWarning: %s\n\n", runErr.Error())
57+
} else {
58+
color.Red("\nError: " + runErr.Error() + "\n\n")
59+
}
5660
} else {
5761
fmt.Printf("Error: %v\n", runErr)
5862
}

boxcli/run.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/pkg/errors"
1212
"github.com/spf13/cobra"
1313
"go.jetpack.io/devbox"
14+
"go.jetpack.io/devbox/nix"
1415
"golang.org/x/exp/slices"
1516
)
1617

@@ -25,7 +26,7 @@ func RunCmd() *cobra.Command {
2526
Short: "Starts a new devbox shell and runs the target script",
2627
Long: "Starts a new interactive shell and runs your target script in it. The shell will exit once your target script is completed or when it is terminated via CTRL-C. Scripts can be defined in your `devbox.json`",
2728
Args: cobra.MaximumNArgs(1),
28-
PersistentPreRunE: nixShellPersistentPreRunE,
29+
PersistentPreRunE: nix.EnsureInstalled,
2930
RunE: func(cmd *cobra.Command, args []string) error {
3031
return runScriptCmd(args, flags)
3132
},

boxcli/shell.go

+2-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/pkg/errors"
1212
"github.com/spf13/cobra"
1313
"go.jetpack.io/devbox"
14+
"go.jetpack.io/devbox/nix"
1415
)
1516

1617
type shellCmdFlags struct {
@@ -29,7 +30,7 @@ func ShellCmd() *cobra.Command {
2930
"In both cases, the shell will be started using the devbox.json found in the --config flag directory. " +
3031
"If --config isn't set, then devbox recursively searches the current directory and its parents.",
3132
Args: validateShellArgs,
32-
PersistentPreRunE: nixShellPersistentPreRunE,
33+
PersistentPreRunE: nix.EnsureInstalled,
3334
RunE: func(cmd *cobra.Command, args []string) error {
3435
return runShellCmd(cmd, args, flags)
3536
},
@@ -75,14 +76,6 @@ func runShellCmd(cmd *cobra.Command, args []string, flags shellCmdFlags) error {
7576
return err
7677
}
7778

78-
func nixShellPersistentPreRunE(cmd *cobra.Command, args []string) error {
79-
_, err := exec.LookPath("nix-shell")
80-
if err != nil {
81-
return errors.New("could not find nix in your PATH\nInstall nix by following the instructions at https://nixos.org/download.html and make sure you've set up your PATH correctly")
82-
}
83-
return nil
84-
}
85-
8679
func validateShellArgs(cmd *cobra.Command, args []string) error {
8780
lenAtDash := cmd.ArgsLenAtDash()
8881
if lenAtDash > 1 {

boxcli/usererr/usererr.go

+23
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ import (
66
"github.com/pkg/errors"
77
)
88

9+
type level int
10+
11+
const (
12+
levelError level = iota
13+
levelWarning
14+
)
15+
916
type combined struct {
1017
source error
1118
userMessage string
19+
level level
1220
}
1321

1422
func New(msg string, args ...any) error {
@@ -17,6 +25,13 @@ func New(msg string, args ...any) error {
1725
})
1826
}
1927

28+
func NewWarning(msg string, args ...any) error {
29+
return errors.WithStack(&combined{
30+
userMessage: fmt.Sprintf(msg, args...),
31+
level: levelWarning,
32+
})
33+
}
34+
2035
func WithUserMessage(source error, msg string, args ...any) error {
2136
if source == nil {
2237
return nil
@@ -32,6 +47,14 @@ func HasUserMessage(err error) bool {
3247
return errors.As(err, &c) // note double pointer
3348
}
3449

50+
func IsWarning(err error) bool {
51+
c := &combined{}
52+
if errors.As(err, &c) {
53+
return c.level == levelWarning
54+
}
55+
return false
56+
}
57+
3558
func (c *combined) Error() string {
3659
if c.source == nil {
3760
return c.userMessage

nix/install.go

+51
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import (
77
"os"
88
"os/exec"
99

10+
"github.com/fatih/color"
1011
"github.com/pkg/errors"
12+
"github.com/spf13/cobra"
13+
"go.jetpack.io/devbox/boxcli/featureflag"
14+
"go.jetpack.io/devbox/boxcli/usererr"
1115
)
1216

1317
//go:embed install.sh
@@ -43,3 +47,50 @@ func Install() error {
4347

4448
return errors.WithStack(cmd.Wait())
4549
}
50+
51+
func EnsureInstalled(cmd *cobra.Command, args []string) error {
52+
if nixBinaryInstalled() {
53+
return nil
54+
}
55+
if nixDirExists() {
56+
// TODO: We may be able to patch the rc files to add nix to the path.
57+
return usererr.New(
58+
"We found a /nix directory but nix binary is not in your PATH. " +
59+
"Try restarting your terminal and running devbox again. If after " +
60+
"restarting you still get this message it's possible nix setup is " +
61+
"missing from your shell rc file. See " +
62+
"https://github.com/NixOS/nix/issues/3616#issuecomment-903869569 for " +
63+
"more details.",
64+
)
65+
}
66+
67+
if featureflag.NixInstaller.Enabled() {
68+
color.Yellow(
69+
"\nNix is not installed. Devbox will attempt to install it. " +
70+
"\n\nPress enter to continue or ctrl-c to exit.\n",
71+
)
72+
fmt.Scanln()
73+
if err := Install(); err != nil {
74+
return err
75+
}
76+
return usererr.NewWarning(
77+
"Nix requires reopening terminal to function correctly. Please open new" +
78+
" terminal and try again.",
79+
)
80+
}
81+
return usererr.New(
82+
"could not find nix in your PATH\nInstall nix by following the " +
83+
"instructions at https://nixos.org/download.html and make sure you've " +
84+
"set up your PATH correctly",
85+
)
86+
}
87+
88+
func nixBinaryInstalled() bool {
89+
_, err := exec.LookPath("nix-shell")
90+
return err == nil
91+
}
92+
93+
func nixDirExists() bool {
94+
_, err := os.Stat("/nix")
95+
return err == nil
96+
}

0 commit comments

Comments
 (0)