diff --git a/internal/boxcli/featureflag/detsys.go b/internal/boxcli/featureflag/detsys.go deleted file mode 100644 index bfc8b457486..00000000000 --- a/internal/boxcli/featureflag/detsys.go +++ /dev/null @@ -1,3 +0,0 @@ -package featureflag - -var UseDetSysInstaller = disable("DETSYS_INSTALLER") diff --git a/internal/boxcli/setup.go b/internal/boxcli/setup.go index 279b7c45c60..b21616b8235 100644 --- a/internal/boxcli/setup.go +++ b/internal/boxcli/setup.go @@ -47,14 +47,13 @@ func runInstallNixCmd(cmd *cobra.Command) error { "Nix is already installed. If this is incorrect "+ "please remove the nix-shell binary from your path.\n", ) - return nil } - return nix.Install(cmd.ErrOrStderr(), nixDaemonFlagVal(cmd)) + return new(nix.Installer).Run(cmd.Context()) } // ensureNixInstalled verifies that nix is installed and that it is of a supported version func ensureNixInstalled(cmd *cobra.Command, _args []string) error { - return nix.EnsureNixInstalled(cmd.ErrOrStderr(), nixDaemonFlagVal(cmd)) + return nix.EnsureNixInstalled(cmd.Context(), cmd.ErrOrStderr(), nixDaemonFlagVal(cmd)) } // We return a closure to avoid printing the warning every time and just diff --git a/internal/nix/install.go b/internal/nix/install.go index 2f8e87e180d..cd0df679bb8 100644 --- a/internal/nix/install.go +++ b/internal/nix/install.go @@ -4,98 +4,23 @@ package nix import ( - "bytes" + "context" _ "embed" "fmt" "io" "os" - "os/exec" - "strings" + "time" + "github.com/briandowns/spinner" "github.com/fatih/color" "github.com/mattn/go-isatty" - "github.com/pkg/errors" - "go.jetpack.io/devbox/internal/boxcli/featureflag" "go.jetpack.io/devbox/internal/boxcli/usererr" - "go.jetpack.io/devbox/internal/build" "go.jetpack.io/devbox/internal/cmdutil" "go.jetpack.io/devbox/internal/fileutil" - "go.jetpack.io/devbox/internal/ux" "go.jetpack.io/devbox/nix" ) -const rootError = "warning: installing Nix as root is not supported by this script!" - -// Install runs the install script for Nix. daemon has 3 states -// nil is unset. false is --no-daemon. true is --daemon. -func Install(writer io.Writer, daemonFn func() *bool) error { - if isRoot() && build.OS() == build.OSWSL { - return usererr.New("Nix cannot be installed as root on WSL. Please run as a normal user with sudo access.") - } - r, w, err := os.Pipe() - if err != nil { - return errors.WithStack(err) - } - defer r.Close() - - installScript := "curl -L https://releases.nixos.org/nix/nix-2.24.7/install | sh -s" - if featureflag.UseDetSysInstaller.Enabled() { - // Should we pin version? Or just trust detsys - installScript = "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install" - if isLinuxWithoutSystemd() { - ux.Fwarningf( - writer, - "Could not detect systemd on your system. Installing Nix in root only mode (--init none).\n", - ) - installScript += " linux --init none" - } - installScript += " --no-confirm" - } else if daemon := daemonFn(); daemon != nil { - if *daemon { - installScript += " -- --daemon" - } else { - installScript += " -- --no-daemon" - } - } - - fmt.Fprintf(writer, "Installing nix with: %s\nThis may require sudo access.\n", installScript) - - cmd := exec.Command("sh", "-c", installScript) - // Attach stdout but no stdin. This makes the command run in non-TTY mode - // which skips the interactive prompts. - // We could attach stderr? but the stdout prompt is pretty useful. - cmd.Stdin = nil - cmd.Stdout = w - cmd.Stderr = w - - err = cmd.Start() - w.Close() - if err != nil { - return errors.WithStack(err) - } - - done := make(chan struct{}) - go func() { - var buf bytes.Buffer - _, err := io.Copy(io.MultiWriter(&buf, os.Stdout), r) - if err != nil { - fmt.Fprintln(writer, err) - } - - if strings.Contains(buf.String(), rootError) { - ux.Finfof( - writer, - "If installing nix as root, consider using the --daemon flag to install in multi-user mode.\n", - ) - } - close(done) - }() - - <-done - return errors.WithStack(cmd.Wait()) -} - func BinaryInstalled() bool { return cmdutil.Exists("nix") } @@ -104,17 +29,13 @@ func dirExists() bool { return fileutil.Exists("/nix") } -func isRoot() bool { - return os.Geteuid() == 0 -} - var ensured = false func Ensured() bool { return ensured } -func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err error) { +func EnsureNixInstalled(ctx context.Context, writer io.Writer, withDaemonFunc func() *bool) (err error) { ensured = true defer func() { if err != nil { @@ -154,31 +75,35 @@ func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err erro color.Yellow("\nNix is not installed. Devbox will attempt to install it.\n\n") + installer := nix.Installer{} if isatty.IsTerminal(os.Stdout.Fd()) { color.Yellow("Press enter to continue or ctrl-c to exit.\n") fmt.Scanln() //nolint:errcheck - } - if err = Install(writer, withDaemonFunc); err != nil { - return err - } + spinny := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(writer)) + spinny.Suffix = " Downloading the Nix installer..." + spinny.Start() + defer spinny.Stop() // reset the terminal in case of a panic - // Source again - if _, err = SourceProfile(); err != nil { + err = installer.Download(ctx) + if err != nil { + return err + } + spinny.Stop() + } else { + fmt.Fprint(writer, "Downloading the Nix installer...") + err = installer.Download(ctx) + if err != nil { + fmt.Fprintln(writer) + return err + } + fmt.Fprintln(writer, " done.") + } + err = installer.Run(ctx) + if err != nil { return err } fmt.Fprintln(writer, "Nix installed successfully. Devbox is ready to use!") return nil } - -func isLinuxWithoutSystemd() bool { - if build.OS() != build.OSLinux { - return false - } - // My best interpretation of https://github.com/DeterminateSystems/nix-installer/blob/66ad2759a3ecb6da345373e3c413c25303305e25/src/action/common/configure_init_service.rs#L108-L118 - if _, err := os.Stat("/run/systemd/system"); errors.Is(err, os.ErrNotExist) { - return true - } - return !cmdutil.Exists("systemctl") -} diff --git a/internal/nix/shim.go b/internal/nix/shim.go index 80592f217ea..7961d2be8a5 100644 --- a/internal/nix/shim.go +++ b/internal/nix/shim.go @@ -1,6 +1,8 @@ package nix -import "go.jetpack.io/devbox/nix" +import ( + "go.jetpack.io/devbox/nix" +) // The types and functions in this file act a shim for the non-internal version // of this package (go.jetpack.io/devbox/nix). That way callers don't need to @@ -26,8 +28,9 @@ const ( ) type ( - Nix = nix.Nix - Info = nix.Info + Nix = nix.Nix + Info = nix.Info + Installer = nix.Installer ) var Default = nix.Default diff --git a/nix/install.go b/nix/install.go new file mode 100644 index 00000000000..c79702aeaf4 --- /dev/null +++ b/nix/install.go @@ -0,0 +1,128 @@ +package nix + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "runtime" +) + +// Installer downloads and installs Nix. +type Installer struct { + // Path is the path to the Nix installer. If it's empty, Download and + // Install will automatically download the installer and set Path to the + // downloaded file before returning. + Path string +} + +// Download downloads the Nix installer without running it. +func (i *Installer) Download(ctx context.Context) error { + if i.Path != "" { + return fmt.Errorf("installer already downloaded: %s", i.Path) + } + + system := "" + switch runtime.GOARCH { + case "amd64": + switch runtime.GOOS { + case "darwin": + system = "x86_64-darwin" + case "linux": + system = "x86_64-linux" + } + case "arm64": + switch runtime.GOOS { + case "darwin": + system = "aarch64-darwin" + case "linux": + system = "aarch64-linux" + } + } + + url := "https://install.determinate.systems/nix/nix-installer-" + system + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("create request: %v", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("do request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("status %s", resp.Status) + } + installer, err := writeTempFile(resp.Body) + if err != nil { + return err + } + err = os.Chmod(installer, 0o755) + if err != nil { + return fmt.Errorf("chmod 0755 installer: %v", err) + } + i.Path = installer + return nil +} + +// Run downloads and installs Nix. +func (i *Installer) Run(ctx context.Context) error { + if i.Path == "" { + err := i.Download(ctx) + if err != nil { + return err + } + } + + cmd := exec.CommandContext(ctx, i.Path, "install") + switch runtime.GOOS { + case "darwin": + cmd.Args = append(cmd.Args, "macos") + case "linux": + cmd.Args = append(cmd.Args, "linux") + _, err := os.Stat("/run/systemd/system") + if errors.Is(err, os.ErrNotExist) { + // Respect any env var settings from the user. + _, ok := os.LookupEnv("NIX_INSTALLER_INIT") + if !ok { + cmd.Args = append(cmd.Args, "--init", "none") + } + } + } + cmd.Args = append(cmd.Args, "--no-confirm") + cmd.Cancel = func() error { + return cmd.Process.Signal(os.Interrupt) + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("run installer: %v", err) + } + _, _ = SourceProfile() + return nil +} + +func writeTempFile(r io.Reader) (path string, err error) { + tempFile, err := os.CreateTemp("", "devbox-nix-installer-") + if err != nil { + return "", fmt.Errorf("create temp file: %v", err) + } + + _, err = io.Copy(tempFile, r) + closeErr := tempFile.Close() + if err == nil && closeErr != nil { + err = fmt.Errorf("close temp file: %v", closeErr) + } + + if err != nil { + os.Remove(tempFile.Name()) + return "", err + } + return tempFile.Name(), nil +}