Skip to content

Commit 9f43428

Browse files
authored
devbox: clean up profile history after sync (#2449)
After syncing the flake packages to the profile, remove all old generations of the Nix profile. This allows the Nix garbage collector to eventually remove any old packages. Opted for a Go implementation instead of calling `nix profile wipe-history` because deleting the history is pretty simple and this is faster.
1 parent b621253 commit 9f43428

File tree

3 files changed

+92
-0
lines changed

3 files changed

+92
-0
lines changed

internal/devbox/nixprofile.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"os"
9+
"path/filepath"
810
"strings"
911

1012
"github.com/samber/lo"
@@ -84,5 +86,42 @@ func (d *Devbox) syncNixProfileFromFlake(ctx context.Context) error {
8486
return fmt.Errorf("error installing packages in nix profile %s: %w", add, err)
8587
}
8688
}
89+
if len(add) > 0 || len(remove) > 0 {
90+
err := wipeProfileHistory(profilePath)
91+
if err != nil {
92+
// Log the error, but nothing terrible happens if this
93+
// fails.
94+
slog.DebugContext(ctx, "error cleaning up profile history", "err", err)
95+
}
96+
}
97+
return nil
98+
}
99+
100+
// wipeProfileHistory removes all old generations of a Nix profile, similar to
101+
// nix profile wipe-history. profile should be a path to the "default" symlink,
102+
// like .devbox/nix/profile/default.
103+
func wipeProfileHistory(profile string) error {
104+
link, err := os.Readlink(profile)
105+
if errors.Is(err, os.ErrNotExist) {
106+
return nil
107+
}
108+
if err != nil {
109+
return err
110+
}
111+
112+
dir := filepath.Dir(profile)
113+
entries, err := os.ReadDir(dir)
114+
if err != nil {
115+
return err
116+
}
117+
for _, dent := range entries {
118+
if dent.Name() == "default" || dent.Name() == link {
119+
continue
120+
}
121+
err := os.Remove(filepath.Join(dir, dent.Name()))
122+
if err != nil && !errors.Is(err, os.ErrNotExist) {
123+
return err
124+
}
125+
}
87126
return nil
88127
}

testscripts/rm/multi.test.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ exec devbox rm vim hello
1010

1111
json.superset devbox.json expected.json
1212

13+
# Check that profile history was cleaned up. There should only be
14+
# default and default-N-link.
15+
glob -count=2 .devbox/nix/profile/*
16+
1317
-- expected.json --
1418
{
1519
"packages": []

testscripts/testrunner/testrunner.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,54 @@ func copyFileCmd(script *testscript.TestScript, neg bool, args []string) {
8080
script.Check(err)
8181
}
8282

83+
func globCmd(script *testscript.TestScript, neg bool, args []string) {
84+
count := -1
85+
if neg {
86+
count = 0
87+
}
88+
if len(args) != 0 {
89+
after, ok := strings.CutPrefix(args[0], "-count=")
90+
if ok {
91+
var err error
92+
count, err = strconv.Atoi(after)
93+
if err != nil {
94+
script.Fatalf("invalid -count=: %v", err)
95+
}
96+
if count < 1 {
97+
script.Fatalf("invalid -count=: must be at least 1")
98+
}
99+
args = args[1:]
100+
}
101+
}
102+
if len(args) == 0 {
103+
script.Fatalf("usage: glob [-count=N] pattern")
104+
}
105+
106+
var matches []string
107+
for _, a := range args {
108+
glob := script.MkAbs(a)
109+
m, err := filepath.Glob(glob)
110+
if err != nil {
111+
script.Fatalf("invalid glob pattern: %v", err)
112+
}
113+
for _, match := range m {
114+
script.Logf("glob %q matched: %s", glob, match)
115+
}
116+
matches = append(matches, m...)
117+
}
118+
119+
// -1 means that no -count= was given, so we want at least 1 match.
120+
if count == -1 {
121+
if len(matches) == 0 && !neg {
122+
script.Fatalf("no matches for globs %q, want at least 1", strings.Join(args, " "))
123+
}
124+
return
125+
}
126+
if len(matches) != count {
127+
script.Fatalf("got %d matches for globs %q, want %d", len(matches), strings.Join(args, " "), count)
128+
}
129+
}
130+
83131
func getTestscriptParams(dir string) testscript.Params {
84132
return testscript.Params{
85133
Dir: dir,
@@ -91,6 +139,7 @@ func getTestscriptParams(dir string) testscript.Params {
91139
"devboxjson.packages.contains": assertDevboxJSONPackagesContains,
92140
"devboxlock.packages.contains": assertDevboxLockPackagesContains,
93141
"env.path.len": assertPathLength,
142+
"glob": globCmd,
94143
"json.superset": assertJSONSuperset,
95144
"path.order": assertPathOrder,
96145
"source.path": sourcePath,

0 commit comments

Comments
 (0)