Skip to content

Commit abc008c

Browse files
authored
[global] Unify nix profile install (#695)
## Summary This fixes a few global issues: * stdout not being attached when doing global install. * Prefetching (and printing) nixpkgs when needed It does so by refactoring some code around: * Unify different ways of installing nix packages (local, global, and utility all use nix.ProfileInstall) * We now always check if nixpkgs is installed, improving messaging when we need to install it. * Moved some nix related stuff to nix packages (calling `nix profile install`, packageInstallIgnore, checking if nixpkgs is installed) What this does not fix: If we install a very large package (e.g. `bazel`) we block on `cmd.Run()` and don't print any output. This affects all ways of installing. ## How was it tested? ```bash devbox global add go_1_20 hello nginx devbox add go_1_19 devbox shell ``` Also tried changing hashes on global devbox.json, installing conflicting binaries, adding and removing large packages (e.g. pulumi, bazel, etc). Bazel was a bad experience.
1 parent b74e651 commit abc008c

File tree

8 files changed

+196
-148
lines changed

8 files changed

+196
-148
lines changed

.github/workflows/cli-tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ jobs:
9999
echo "NIX_SSL_CERT_FILE=$NIX_SSL_CERT_FILE" >> $GITHUB_ENV
100100
echo "PATH=$PATH" >> $GITHUB_ENV
101101
- name: Run tests
102-
run: go test -race -cover ./...
102+
run: go test -race -cover -v ./...
103103

104104
auto-nix-install: # ensure Devbox installs nix and works properly after installation.
105105
runs-on: ubuntu-latest
@@ -114,4 +114,4 @@ jobs:
114114
- name: Install nix and devbox packages
115115
run: |
116116
NIX_INSTALLER_NO_CHANNEL_ADD=1
117-
devbox shell -- echo "Installing packages..."
117+
devbox run echo "Installing packages..."

internal/impl/devbox.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ func (d *Devbox) StartProcessManager(ctx context.Context) error {
629629
processComposePath, err := utilityLookPath("process-compose")
630630
if err != nil {
631631
fmt.Fprintln(d.writer, "Installing process-compose. This may take a minute but will only happen once.")
632-
if err = addDevboxUtilityPackage("process-compose"); err != nil {
632+
if err = d.addDevboxUtilityPackage("process-compose"); err != nil {
633633
return err
634634
}
635635
}
@@ -868,7 +868,7 @@ func (d *Devbox) installNixProfile() (err error) {
868868
)
869869

870870
cmd.Env = nix.DefaultEnv()
871-
cmd.Stdout = &nixPackageInstallWriter{d.writer}
871+
cmd.Stdout = &nix.PackageInstallWriter{Writer: d.writer}
872872

873873
cmd.Stderr = cmd.Stdout
874874

internal/impl/global.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,18 @@ func (d *Devbox) AddGlobal(pkgs ...string) error {
4242
if err != nil {
4343
return err
4444
}
45-
for _, pkg := range pkgs {
46-
if err := nix.ProfileInstall(profilePath, plansdk.DefaultNixpkgsCommit, pkg); err != nil {
45+
46+
total := len(pkgs)
47+
for idx, pkg := range pkgs {
48+
stepNum := idx + 1
49+
stepMsg := fmt.Sprintf("[%d/%d] %s", stepNum, total, pkg)
50+
if err := nix.ProfileInstall(&nix.ProfileInstallArgs{
51+
CustomStepMessage: stepMsg,
52+
NixpkgsCommit: d.cfg.Nixpkgs.Commit,
53+
Package: pkg,
54+
ProfilePath: profilePath,
55+
Writer: d.writer,
56+
}); err != nil {
4757
fmt.Fprintf(d.writer, "Error installing %s: %s", pkg, err)
4858
} else {
4959
fmt.Fprintf(d.writer, "%s is now installed\n", pkg)

internal/impl/packages.go

Lines changed: 10 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package impl
22

33
import (
4-
"encoding/json"
54
"fmt"
6-
"io/fs"
75
"os"
86
"os/exec"
97
"path/filepath"
108
"strings"
119

12-
"github.com/fatih/color"
1310
"github.com/pkg/errors"
1411
"go.jetpack.io/devbox/internal/boxcli/featureflag"
1512
"go.jetpack.io/devbox/internal/debug"
1613
"go.jetpack.io/devbox/internal/fileutil"
1714
"go.jetpack.io/devbox/internal/nix"
18-
"go.jetpack.io/devbox/internal/xdg"
1915
)
2016

2117
// packages.go has functions for adding, removing and getting info about nix packages
@@ -53,10 +49,6 @@ func (d *Devbox) addPackagesToProfile(mode installMode) error {
5349
return nil
5450
}
5551

56-
if err := d.ensureNixpkgsPrefetched(); err != nil {
57-
return err
58-
}
59-
6052
pkgs, err := d.pendingPackagesForInstallation()
6153
if err != nil {
6254
return err
@@ -84,29 +76,17 @@ func (d *Devbox) addPackagesToProfile(mode installMode) error {
8476
stepNum := idx + 1
8577

8678
stepMsg := fmt.Sprintf("[%d/%d] %s", stepNum, total, pkg)
87-
fmt.Printf("%s\n", stepMsg)
88-
89-
cmd := exec.Command(
90-
"nix", "profile", "install",
91-
"--profile", profileDir,
92-
"--priority", d.getPackagePriority(pkg),
93-
"--impure", // Needed to allow flags from environment to be used.
94-
nix.FlakeNixpkgs(d.cfg.Nixpkgs.Commit)+"#"+pkg,
95-
)
96-
cmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)
97-
cmd.Stdout = &nixPackageInstallWriter{d.writer}
9879

99-
cmd.Env = nix.DefaultEnv()
100-
cmd.Stderr = cmd.Stdout
101-
err = cmd.Run()
102-
if err != nil {
103-
fmt.Fprintf(d.writer, "%s: ", stepMsg)
104-
color.New(color.FgRed).Fprintf(d.writer, "Fail\n")
105-
return errors.Wrapf(err, "Command: %s", cmd)
80+
if err := nix.ProfileInstall(&nix.ProfileInstallArgs{
81+
CustomStepMessage: stepMsg,
82+
ExtraFlags: []string{"--priority", d.getPackagePriority(pkg)},
83+
NixpkgsCommit: d.cfg.Nixpkgs.Commit,
84+
Package: pkg,
85+
ProfilePath: profileDir,
86+
Writer: d.writer,
87+
}); err != nil {
88+
return err
10689
}
107-
108-
fmt.Fprintf(d.writer, "%s: ", stepMsg)
109-
color.New(color.FgGreen).Fprintf(d.writer, "Success\n")
11090
}
11191

11292
return nil
@@ -146,6 +126,7 @@ func (d *Devbox) removePackagesFromProfile(pkgs []string) error {
146126
return errors.Errorf("Did not find AttributePath for package: %s", pkg)
147127
}
148128

129+
// TODO: unify this with nix.ProfileRemove
149130
cmd := exec.Command("nix", "profile", "remove",
150131
"--profile", profileDir,
151132
attrPath,
@@ -240,99 +221,3 @@ func resetProfileDirForFlakes(profileDir string) (err error) {
240221

241222
return errors.WithStack(os.Remove(profileDir))
242223
}
243-
244-
// ensureNixpkgsPrefetched runs the prefetch step to download the flake of the registry
245-
func (d *Devbox) ensureNixpkgsPrefetched() error {
246-
// Look up the cached map of commitHash:nixStoreLocation
247-
commitToLocation, err := d.nixpkgsCommitFileContents()
248-
if err != nil {
249-
return err
250-
}
251-
252-
// Check if this nixpkgs.Commit is located in the local /nix/store
253-
location, isPresent := commitToLocation[d.cfg.Nixpkgs.Commit]
254-
if isPresent {
255-
if fi, err := os.Stat(location); err == nil && fi.IsDir() {
256-
// The nixpkgs for this commit hash is present, so we don't need to prefetch
257-
return nil
258-
}
259-
}
260-
261-
fmt.Fprintf(d.writer, "Ensuring nixpkgs registry is downloaded.\n")
262-
cmd := exec.Command(
263-
"nix", "flake", "prefetch",
264-
nix.FlakeNixpkgs(d.cfg.Nixpkgs.Commit),
265-
)
266-
cmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)
267-
cmd.Env = nix.DefaultEnv()
268-
cmd.Stdout = d.writer
269-
cmd.Stderr = cmd.Stdout
270-
if err := cmd.Run(); err != nil {
271-
fmt.Fprintf(d.writer, "Ensuring nixpkgs registry is downloaded: ")
272-
color.New(color.FgRed).Fprintf(d.writer, "Fail\n")
273-
return errors.Wrapf(err, "Command: %s", cmd)
274-
}
275-
fmt.Fprintf(d.writer, "Ensuring nixpkgs registry is downloaded: ")
276-
color.New(color.FgGreen).Fprintf(d.writer, "Success\n")
277-
278-
return d.saveToNixpkgsCommitFile(commitToLocation)
279-
}
280-
281-
func (d *Devbox) nixpkgsCommitFileContents() (map[string]string, error) {
282-
path := nixpkgsCommitFilePath()
283-
if !fileutil.Exists(path) {
284-
return map[string]string{}, nil
285-
}
286-
287-
contents, err := os.ReadFile(path)
288-
if err != nil {
289-
return nil, errors.WithStack(err)
290-
}
291-
292-
commitToLocation := map[string]string{}
293-
if err := json.Unmarshal(contents, &commitToLocation); err != nil {
294-
return nil, errors.WithStack(err)
295-
}
296-
return commitToLocation, nil
297-
}
298-
299-
func (d *Devbox) saveToNixpkgsCommitFile(commitToLocation map[string]string) error {
300-
// Make a query to get the /nix/store path for this commit hash.
301-
cmd := exec.Command("nix", "flake", "prefetch", "--json",
302-
nix.FlakeNixpkgs(d.cfg.Nixpkgs.Commit),
303-
)
304-
cmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)
305-
out, err := cmd.Output()
306-
if err != nil {
307-
return errors.WithStack(err)
308-
}
309-
310-
// read the json response
311-
var prefetchData struct {
312-
StorePath string `json:"storePath"`
313-
}
314-
if err := json.Unmarshal(out, &prefetchData); err != nil {
315-
return errors.WithStack(err)
316-
}
317-
318-
// Ensure the nixpkgs commit file path exists so we can write an update to it
319-
path := nixpkgsCommitFilePath()
320-
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil && !errors.Is(err, fs.ErrExist) {
321-
return errors.WithStack(err)
322-
}
323-
324-
// write to the map, jsonify it, and write that json to the nixpkgsCommit file
325-
commitToLocation[d.cfg.Nixpkgs.Commit] = prefetchData.StorePath
326-
serialized, err := json.Marshal(commitToLocation)
327-
if err != nil {
328-
return errors.WithStack(err)
329-
}
330-
331-
err = os.WriteFile(path, serialized, 0644)
332-
return errors.WithStack(err)
333-
}
334-
335-
func nixpkgsCommitFilePath() string {
336-
cacheDir := xdg.CacheSubpath("devbox")
337-
return filepath.Join(cacheDir, "nixpkgs.json")
338-
}

internal/impl/util.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ const nixpkgsUtilityCommit = "f7475ce8950b761d80a13f3f81d2c23fce60c1dd"
1717
// It's used to install applications devbox might need, like process-compose
1818
// This is an alternative to a global install which would modify a user's
1919
// environment.
20-
func addDevboxUtilityPackage(pkg string) error {
20+
func (d *Devbox) addDevboxUtilityPackage(pkg string) error {
2121
profilePath, err := utilityNixProfilePath()
2222
if err != nil {
2323
return err
2424
}
25-
return nix.ProfileInstall(profilePath, nixpkgsUtilityCommit, pkg)
25+
return nix.ProfileInstall(&nix.ProfileInstallArgs{
26+
NixpkgsCommit: nixpkgsUtilityCommit,
27+
Package: pkg,
28+
ProfilePath: profilePath,
29+
Writer: d.writer,
30+
})
2631
}
2732

2833
func utilityLookPath(binName string) (string, error) {

internal/nix/nixpkgs.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package nix
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"io/fs"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
12+
"github.com/fatih/color"
13+
"github.com/pkg/errors"
14+
"go.jetpack.io/devbox/internal/fileutil"
15+
"go.jetpack.io/devbox/internal/xdg"
16+
)
17+
18+
// ensureNixpkgsPrefetched runs the prefetch step to download the flake of the registry
19+
func ensureNixpkgsPrefetched(w io.Writer, commit string) error {
20+
// Look up the cached map of commitHash:nixStoreLocation
21+
commitToLocation, err := nixpkgsCommitFileContents()
22+
if err != nil {
23+
return err
24+
}
25+
26+
// Check if this nixpkgs.Commit is located in the local /nix/store
27+
location, isPresent := commitToLocation[commit]
28+
if isPresent {
29+
if fi, err := os.Stat(location); err == nil && fi.IsDir() {
30+
// The nixpkgs for this commit hash is present, so we don't need to prefetch
31+
return nil
32+
}
33+
}
34+
35+
fmt.Fprintf(w, "Ensuring nixpkgs registry is downloaded.\n")
36+
cmd := exec.Command(
37+
"nix", "flake", "prefetch",
38+
FlakeNixpkgs(commit),
39+
)
40+
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
41+
cmd.Env = DefaultEnv()
42+
cmd.Stdout = w
43+
cmd.Stderr = cmd.Stdout
44+
if err := cmd.Run(); err != nil {
45+
fmt.Fprintf(w, "Ensuring nixpkgs registry is downloaded: ")
46+
color.New(color.FgRed).Fprintf(w, "Fail\n")
47+
return errors.Wrapf(err, "Command: %s", cmd)
48+
}
49+
fmt.Fprintf(w, "Ensuring nixpkgs registry is downloaded: ")
50+
color.New(color.FgGreen).Fprintf(w, "Success\n")
51+
52+
return saveToNixpkgsCommitFile(commit, commitToLocation)
53+
}
54+
55+
func nixpkgsCommitFileContents() (map[string]string, error) {
56+
path := nixpkgsCommitFilePath()
57+
if !fileutil.Exists(path) {
58+
return map[string]string{}, nil
59+
}
60+
61+
contents, err := os.ReadFile(path)
62+
if err != nil {
63+
return nil, errors.WithStack(err)
64+
}
65+
66+
commitToLocation := map[string]string{}
67+
if err := json.Unmarshal(contents, &commitToLocation); err != nil {
68+
return nil, errors.WithStack(err)
69+
}
70+
return commitToLocation, nil
71+
}
72+
73+
func saveToNixpkgsCommitFile(commit string, commitToLocation map[string]string) error {
74+
// Make a query to get the /nix/store path for this commit hash.
75+
cmd := exec.Command("nix", "flake", "prefetch", "--json",
76+
FlakeNixpkgs(commit),
77+
)
78+
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
79+
out, err := cmd.Output()
80+
if err != nil {
81+
return errors.WithStack(err)
82+
}
83+
84+
// read the json response
85+
var prefetchData struct {
86+
StorePath string `json:"storePath"`
87+
}
88+
if err := json.Unmarshal(out, &prefetchData); err != nil {
89+
return errors.WithStack(err)
90+
}
91+
92+
// Ensure the nixpkgs commit file path exists so we can write an update to it
93+
path := nixpkgsCommitFilePath()
94+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil && !errors.Is(err, fs.ErrExist) {
95+
return errors.WithStack(err)
96+
}
97+
98+
// write to the map, jsonify it, and write that json to the nixpkgsCommit file
99+
commitToLocation[commit] = prefetchData.StorePath
100+
serialized, err := json.Marshal(commitToLocation)
101+
if err != nil {
102+
return errors.WithStack(err)
103+
}
104+
105+
err = os.WriteFile(path, serialized, 0644)
106+
return errors.WithStack(err)
107+
}
108+
109+
func nixpkgsCommitFilePath() string {
110+
cacheDir := xdg.CacheSubpath("devbox")
111+
return filepath.Join(cacheDir, "nixpkgs.json")
112+
}

0 commit comments

Comments
 (0)