Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions internal/boxcli/featureflag/detsys.go

This file was deleted.

5 changes: 2 additions & 3 deletions internal/boxcli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 25 additions & 100 deletions internal/nix/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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..."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: drop the prefix space?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this puts a space between the spinner and the message. I copied what we have for Computing the Devbox environment.

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")
}
9 changes: 6 additions & 3 deletions internal/nix/shim.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
128 changes: 128 additions & 0 deletions nix/install.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading