Skip to content

Commit d4325c9

Browse files
authored
nix: make DetSys installer the default (#2447)
Remove the feature flag and make the DetSys installer the default. - Download the installer directly in case the system doesn't have curl. - Expose the auto-install functionality in the public nix package so we can reuse it elsewhere. - Remove some checks that were only relevant to the old installer (WSL2 limitations and warnings about root). The install logic is also now part of an `Installer` struct to provide some flexibility around what install program is run and when it's downloaded. type Installer struct{ ... } func (i *Installer) Download(ctx context.Context) error func (i *Installer) Run(ctx context.Context) error
1 parent 6d759b4 commit d4325c9

File tree

5 files changed

+161
-109
lines changed

5 files changed

+161
-109
lines changed

internal/boxcli/featureflag/detsys.go

Lines changed: 0 additions & 3 deletions
This file was deleted.

internal/boxcli/setup.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,13 @@ func runInstallNixCmd(cmd *cobra.Command) error {
4747
"Nix is already installed. If this is incorrect "+
4848
"please remove the nix-shell binary from your path.\n",
4949
)
50-
return nil
5150
}
52-
return nix.Install(cmd.ErrOrStderr(), nixDaemonFlagVal(cmd))
51+
return new(nix.Installer).Run(cmd.Context())
5352
}
5453

5554
// ensureNixInstalled verifies that nix is installed and that it is of a supported version
5655
func ensureNixInstalled(cmd *cobra.Command, _args []string) error {
57-
return nix.EnsureNixInstalled(cmd.ErrOrStderr(), nixDaemonFlagVal(cmd))
56+
return nix.EnsureNixInstalled(cmd.Context(), cmd.ErrOrStderr(), nixDaemonFlagVal(cmd))
5857
}
5958

6059
// We return a closure to avoid printing the warning every time and just

internal/nix/install.go

Lines changed: 25 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,98 +4,23 @@
44
package nix
55

66
import (
7-
"bytes"
7+
"context"
88
_ "embed"
99
"fmt"
1010
"io"
1111
"os"
12-
"os/exec"
13-
"strings"
12+
"time"
1413

14+
"github.com/briandowns/spinner"
1515
"github.com/fatih/color"
1616
"github.com/mattn/go-isatty"
17-
"github.com/pkg/errors"
1817

19-
"go.jetpack.io/devbox/internal/boxcli/featureflag"
2018
"go.jetpack.io/devbox/internal/boxcli/usererr"
21-
"go.jetpack.io/devbox/internal/build"
2219
"go.jetpack.io/devbox/internal/cmdutil"
2320
"go.jetpack.io/devbox/internal/fileutil"
24-
"go.jetpack.io/devbox/internal/ux"
2521
"go.jetpack.io/devbox/nix"
2622
)
2723

28-
const rootError = "warning: installing Nix as root is not supported by this script!"
29-
30-
// Install runs the install script for Nix. daemon has 3 states
31-
// nil is unset. false is --no-daemon. true is --daemon.
32-
func Install(writer io.Writer, daemonFn func() *bool) error {
33-
if isRoot() && build.OS() == build.OSWSL {
34-
return usererr.New("Nix cannot be installed as root on WSL. Please run as a normal user with sudo access.")
35-
}
36-
r, w, err := os.Pipe()
37-
if err != nil {
38-
return errors.WithStack(err)
39-
}
40-
defer r.Close()
41-
42-
installScript := "curl -L https://releases.nixos.org/nix/nix-2.24.7/install | sh -s"
43-
if featureflag.UseDetSysInstaller.Enabled() {
44-
// Should we pin version? Or just trust detsys
45-
installScript = "curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install"
46-
if isLinuxWithoutSystemd() {
47-
ux.Fwarningf(
48-
writer,
49-
"Could not detect systemd on your system. Installing Nix in root only mode (--init none).\n",
50-
)
51-
installScript += " linux --init none"
52-
}
53-
installScript += " --no-confirm"
54-
} else if daemon := daemonFn(); daemon != nil {
55-
if *daemon {
56-
installScript += " -- --daemon"
57-
} else {
58-
installScript += " -- --no-daemon"
59-
}
60-
}
61-
62-
fmt.Fprintf(writer, "Installing nix with: %s\nThis may require sudo access.\n", installScript)
63-
64-
cmd := exec.Command("sh", "-c", installScript)
65-
// Attach stdout but no stdin. This makes the command run in non-TTY mode
66-
// which skips the interactive prompts.
67-
// We could attach stderr? but the stdout prompt is pretty useful.
68-
cmd.Stdin = nil
69-
cmd.Stdout = w
70-
cmd.Stderr = w
71-
72-
err = cmd.Start()
73-
w.Close()
74-
if err != nil {
75-
return errors.WithStack(err)
76-
}
77-
78-
done := make(chan struct{})
79-
go func() {
80-
var buf bytes.Buffer
81-
_, err := io.Copy(io.MultiWriter(&buf, os.Stdout), r)
82-
if err != nil {
83-
fmt.Fprintln(writer, err)
84-
}
85-
86-
if strings.Contains(buf.String(), rootError) {
87-
ux.Finfof(
88-
writer,
89-
"If installing nix as root, consider using the --daemon flag to install in multi-user mode.\n",
90-
)
91-
}
92-
close(done)
93-
}()
94-
95-
<-done
96-
return errors.WithStack(cmd.Wait())
97-
}
98-
9924
func BinaryInstalled() bool {
10025
return cmdutil.Exists("nix")
10126
}
@@ -104,17 +29,13 @@ func dirExists() bool {
10429
return fileutil.Exists("/nix")
10530
}
10631

107-
func isRoot() bool {
108-
return os.Geteuid() == 0
109-
}
110-
11132
var ensured = false
11233

11334
func Ensured() bool {
11435
return ensured
11536
}
11637

117-
func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err error) {
38+
func EnsureNixInstalled(ctx context.Context, writer io.Writer, withDaemonFunc func() *bool) (err error) {
11839
ensured = true
11940
defer func() {
12041
if err != nil {
@@ -154,31 +75,35 @@ func EnsureNixInstalled(writer io.Writer, withDaemonFunc func() *bool) (err erro
15475

15576
color.Yellow("\nNix is not installed. Devbox will attempt to install it.\n\n")
15677

78+
installer := nix.Installer{}
15779
if isatty.IsTerminal(os.Stdout.Fd()) {
15880
color.Yellow("Press enter to continue or ctrl-c to exit.\n")
15981
fmt.Scanln() //nolint:errcheck
160-
}
16182

162-
if err = Install(writer, withDaemonFunc); err != nil {
163-
return err
164-
}
83+
spinny := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(writer))
84+
spinny.Suffix = " Downloading the Nix installer..."
85+
spinny.Start()
86+
defer spinny.Stop() // reset the terminal in case of a panic
16587

166-
// Source again
167-
if _, err = SourceProfile(); err != nil {
88+
err = installer.Download(ctx)
89+
if err != nil {
90+
return err
91+
}
92+
spinny.Stop()
93+
} else {
94+
fmt.Fprint(writer, "Downloading the Nix installer...")
95+
err = installer.Download(ctx)
96+
if err != nil {
97+
fmt.Fprintln(writer)
98+
return err
99+
}
100+
fmt.Fprintln(writer, " done.")
101+
}
102+
err = installer.Run(ctx)
103+
if err != nil {
168104
return err
169105
}
170106

171107
fmt.Fprintln(writer, "Nix installed successfully. Devbox is ready to use!")
172108
return nil
173109
}
174-
175-
func isLinuxWithoutSystemd() bool {
176-
if build.OS() != build.OSLinux {
177-
return false
178-
}
179-
// My best interpretation of https://github.com/DeterminateSystems/nix-installer/blob/66ad2759a3ecb6da345373e3c413c25303305e25/src/action/common/configure_init_service.rs#L108-L118
180-
if _, err := os.Stat("/run/systemd/system"); errors.Is(err, os.ErrNotExist) {
181-
return true
182-
}
183-
return !cmdutil.Exists("systemctl")
184-
}

internal/nix/shim.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package nix
22

3-
import "go.jetpack.io/devbox/nix"
3+
import (
4+
"go.jetpack.io/devbox/nix"
5+
)
46

57
// The types and functions in this file act a shim for the non-internal version
68
// of this package (go.jetpack.io/devbox/nix). That way callers don't need to
@@ -26,8 +28,9 @@ const (
2628
)
2729

2830
type (
29-
Nix = nix.Nix
30-
Info = nix.Info
31+
Nix = nix.Nix
32+
Info = nix.Info
33+
Installer = nix.Installer
3134
)
3235

3336
var Default = nix.Default

nix/install.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package nix
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"os/exec"
11+
"runtime"
12+
)
13+
14+
// Installer downloads and installs Nix.
15+
type Installer struct {
16+
// Path is the path to the Nix installer. If it's empty, Download and
17+
// Install will automatically download the installer and set Path to the
18+
// downloaded file before returning.
19+
Path string
20+
}
21+
22+
// Download downloads the Nix installer without running it.
23+
func (i *Installer) Download(ctx context.Context) error {
24+
if i.Path != "" {
25+
return fmt.Errorf("installer already downloaded: %s", i.Path)
26+
}
27+
28+
system := ""
29+
switch runtime.GOARCH {
30+
case "amd64":
31+
switch runtime.GOOS {
32+
case "darwin":
33+
system = "x86_64-darwin"
34+
case "linux":
35+
system = "x86_64-linux"
36+
}
37+
case "arm64":
38+
switch runtime.GOOS {
39+
case "darwin":
40+
system = "aarch64-darwin"
41+
case "linux":
42+
system = "aarch64-linux"
43+
}
44+
}
45+
46+
url := "https://install.determinate.systems/nix/nix-installer-" + system
47+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
48+
if err != nil {
49+
return fmt.Errorf("create request: %v", err)
50+
}
51+
52+
resp, err := http.DefaultClient.Do(req)
53+
if err != nil {
54+
return fmt.Errorf("do request: %v", err)
55+
}
56+
defer resp.Body.Close()
57+
58+
if resp.StatusCode != http.StatusOK {
59+
return fmt.Errorf("status %s", resp.Status)
60+
}
61+
installer, err := writeTempFile(resp.Body)
62+
if err != nil {
63+
return err
64+
}
65+
err = os.Chmod(installer, 0o755)
66+
if err != nil {
67+
return fmt.Errorf("chmod 0755 installer: %v", err)
68+
}
69+
i.Path = installer
70+
return nil
71+
}
72+
73+
// Run downloads and installs Nix.
74+
func (i *Installer) Run(ctx context.Context) error {
75+
if i.Path == "" {
76+
err := i.Download(ctx)
77+
if err != nil {
78+
return err
79+
}
80+
}
81+
82+
cmd := exec.CommandContext(ctx, i.Path, "install")
83+
switch runtime.GOOS {
84+
case "darwin":
85+
cmd.Args = append(cmd.Args, "macos")
86+
case "linux":
87+
cmd.Args = append(cmd.Args, "linux")
88+
_, err := os.Stat("/run/systemd/system")
89+
if errors.Is(err, os.ErrNotExist) {
90+
// Respect any env var settings from the user.
91+
_, ok := os.LookupEnv("NIX_INSTALLER_INIT")
92+
if !ok {
93+
cmd.Args = append(cmd.Args, "--init", "none")
94+
}
95+
}
96+
}
97+
cmd.Args = append(cmd.Args, "--no-confirm")
98+
cmd.Cancel = func() error {
99+
return cmd.Process.Signal(os.Interrupt)
100+
}
101+
cmd.Stdin = os.Stdin
102+
cmd.Stdout = os.Stdout
103+
cmd.Stderr = os.Stderr
104+
if err := cmd.Run(); err != nil {
105+
return fmt.Errorf("run installer: %v", err)
106+
}
107+
_, _ = SourceProfile()
108+
return nil
109+
}
110+
111+
func writeTempFile(r io.Reader) (path string, err error) {
112+
tempFile, err := os.CreateTemp("", "devbox-nix-installer-")
113+
if err != nil {
114+
return "", fmt.Errorf("create temp file: %v", err)
115+
}
116+
117+
_, err = io.Copy(tempFile, r)
118+
closeErr := tempFile.Close()
119+
if err == nil && closeErr != nil {
120+
err = fmt.Errorf("close temp file: %v", closeErr)
121+
}
122+
123+
if err != nil {
124+
os.Remove(tempFile.Name())
125+
return "", err
126+
}
127+
return tempFile.Name(), nil
128+
}

0 commit comments

Comments
 (0)