Skip to content

Commit c279bb4

Browse files
committed
nix profile from flake
1 parent a0fa00a commit c279bb4

File tree

2 files changed

+96
-161
lines changed

2 files changed

+96
-161
lines changed

internal/devbox/nixprofile.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package devbox
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"os/exec"
8+
"slices"
9+
10+
"go.jetpack.io/devbox/internal/nix"
11+
)
12+
13+
func syncFlakeToProfile(ctx context.Context, flakePath, profilePath string) error {
14+
cmd := exec.CommandContext(ctx, "nix", "eval", "--json", flakePath+"#devShells."+nix.System()+".default.buildInputs")
15+
b, err := cmd.Output()
16+
if err != nil {
17+
return fmt.Errorf("nix eval devShells: %v", err)
18+
}
19+
storePaths := []string{}
20+
if err := json.Unmarshal(b, &storePaths); err != nil {
21+
return fmt.Errorf("unmarshal store paths: %s: %v", b, err)
22+
}
23+
24+
listCmd := exec.CommandContext(ctx, "nix", "profile", "list", "--json", "--profile", profilePath)
25+
b, err = listCmd.Output()
26+
if err != nil {
27+
return err
28+
}
29+
var profile struct {
30+
Elements []struct {
31+
StorePaths []string
32+
}
33+
}
34+
if err := json.Unmarshal(b, &profile); err != nil {
35+
return fmt.Errorf("unmarshal profile: %v", err)
36+
}
37+
got := make([]string, 0, len(profile.Elements))
38+
for _, e := range profile.Elements {
39+
got = append(got, e.StorePaths...)
40+
}
41+
42+
add, remove := diffStorePaths(got, storePaths)
43+
if len(remove) > 0 {
44+
removeCmd := exec.CommandContext(ctx, "nix", "profile", "remove", "--profile", profilePath)
45+
removeCmd.Args = append(removeCmd.Args, remove...)
46+
if err := removeCmd.Run(); err != nil {
47+
return err
48+
}
49+
}
50+
if len(add) > 0 {
51+
addCmd := exec.CommandContext(ctx, "nix", "profile", "install", "--profile", profilePath)
52+
addCmd.Args = append(addCmd.Args, add...)
53+
if err := addCmd.Run(); err != nil {
54+
return err
55+
}
56+
}
57+
return nil
58+
}
59+
60+
func diffStorePaths(got, want []string) (add, remove []string) {
61+
slices.Sort(got)
62+
slices.Sort(want)
63+
64+
var g, w int
65+
for {
66+
if g >= len(got) {
67+
add = append(add, want[w:]...)
68+
break
69+
}
70+
if w >= len(want) {
71+
remove = append(remove, got[g:]...)
72+
break
73+
}
74+
75+
switch {
76+
case got[g] == want[w]:
77+
g++
78+
w++
79+
case got[g] < want[w]:
80+
remove = append(remove, got[g])
81+
g++
82+
case got[g] > want[w]:
83+
add = append(add, want[w])
84+
w++
85+
}
86+
}
87+
return add, remove
88+
}

internal/devbox/packages.go

Lines changed: 8 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"go.jetpack.io/devbox/internal/devbox/devopt"
1919
"go.jetpack.io/devbox/internal/devpkg"
2020
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
21-
"go.jetpack.io/devbox/internal/nix/nixprofile"
2221
"go.jetpack.io/devbox/internal/shellgen"
2322

2423
"go.jetpack.io/devbox/internal/boxcli/usererr"
@@ -274,10 +273,6 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
274273
}
275274
}
276275

277-
if err := d.syncPackagesToProfile(ctx, mode); err != nil {
278-
return err
279-
}
280-
281276
if err := d.InstallRunXPackages(ctx); err != nil {
282277
return err
283278
}
@@ -297,6 +292,14 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
297292
return err
298293
}
299294

295+
profile, err := d.profilePath()
296+
if err != nil {
297+
return err
298+
}
299+
if err := syncFlakeToProfile(ctx, d.flakeDir(), profile); err != nil {
300+
return err
301+
}
302+
300303
// Ensure we clean out packages that are no longer needed.
301304
d.lockfile.Tidy()
302305

@@ -331,162 +334,6 @@ func (d *Devbox) profilePath() (string, error) {
331334
return absPath, errors.WithStack(os.MkdirAll(filepath.Dir(absPath), 0o755))
332335
}
333336

334-
// syncPackagesToProfile can ensure that all packages in devbox.json exist in the nix profile,
335-
// and no more. However, it may skip some steps depending on the `mode`.
336-
func (d *Devbox) syncPackagesToProfile(ctx context.Context, mode installMode) error {
337-
defer debug.FunctionTimer().End()
338-
defer trace.StartRegion(ctx, "syncPackagesToProfile").End()
339-
340-
// First, fetch the profile items from the nix-profile,
341-
// and get the installable packages
342-
profileDir, err := d.profilePath()
343-
if err != nil {
344-
return err
345-
}
346-
profileItems, err := nixprofile.ProfileListItems(d.stderr, profileDir)
347-
if err != nil {
348-
return err
349-
}
350-
packages, err := d.AllInstallablePackages()
351-
if err != nil {
352-
return err
353-
}
354-
355-
// Remove non-nix packages from the list
356-
packages = lo.Filter(packages, devpkg.IsNix)
357-
358-
if err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {
359-
return err
360-
}
361-
362-
// Second, remove any packages from the nix-profile that are not in the config
363-
itemsToKeep := profileItems
364-
if mode != install {
365-
itemsToKeep, err = d.removeExtraItemsFromProfile(ctx, profileDir, profileItems, packages)
366-
if err != nil {
367-
return err
368-
}
369-
}
370-
371-
// we are done if mode is uninstall
372-
if mode == uninstall {
373-
return nil
374-
}
375-
376-
// Last, find the pending packages, and ensure they are added to the nix-profile
377-
// Important to maintain the order of packages as specified by
378-
// Devbox.InstallablePackages() (higher priority first).
379-
// We also run nix profile upgrade on any virtenv flakes. This is a bit of a
380-
// blunt approach, but ensured any plugin created flakes are up-to-date.
381-
pending := []*devpkg.Package{}
382-
for _, pkg := range packages {
383-
idx, err := nixprofile.ProfileListIndex(&nixprofile.ProfileListIndexArgs{
384-
Items: itemsToKeep,
385-
Lockfile: d.lockfile,
386-
Writer: d.stderr,
387-
Package: pkg,
388-
ProfileDir: profileDir,
389-
})
390-
if err != nil {
391-
if !errors.Is(err, nix.ErrPackageNotFound) {
392-
return err
393-
}
394-
pending = append(pending, pkg)
395-
} else if f, err := pkg.FlakeInstallable(); err == nil && d.pluginManager.PathIsInVirtenv(f.Ref.Path) {
396-
if err := nix.ProfileUpgrade(profileDir, idx); err != nil {
397-
return err
398-
}
399-
}
400-
}
401-
402-
return d.addPackagesToProfile(ctx, pending)
403-
}
404-
405-
func (d *Devbox) removeExtraItemsFromProfile(
406-
ctx context.Context,
407-
profileDir string,
408-
profileItems []*nixprofile.NixProfileListItem,
409-
packages []*devpkg.Package,
410-
) ([]*nixprofile.NixProfileListItem, error) {
411-
defer debug.FunctionTimer().End()
412-
defer trace.StartRegion(ctx, "removeExtraPackagesFromProfile").End()
413-
414-
itemsToKeep := []*nixprofile.NixProfileListItem{}
415-
extras := []*nixprofile.NixProfileListItem{}
416-
// Note: because devpkg.Package uses memoization when normalizing attribute paths (slow operation),
417-
// and since we're reusing the Package objects, this O(n*m) loop becomes O(n+m) wrt the slow operation.
418-
for _, item := range profileItems {
419-
found := false
420-
for _, pkg := range packages {
421-
if item.Matches(pkg, d.lockfile) {
422-
itemsToKeep = append(itemsToKeep, item)
423-
found = true
424-
break
425-
}
426-
}
427-
if !found {
428-
extras = append(extras, item)
429-
}
430-
}
431-
// Remove by index to avoid comparing nix.ProfileListItem <> nix.Inputs again.
432-
if err := nixprofile.ProfileRemoveItems(profileDir, extras); err != nil {
433-
return nil, err
434-
}
435-
return itemsToKeep, nil
436-
}
437-
438-
// addPackagesToProfile inspects the packages in devbox.json, checks which of them
439-
// are missing from the nix profile, and then installs each package individually into the
440-
// nix profile.
441-
func (d *Devbox) addPackagesToProfile(ctx context.Context, pkgs []*devpkg.Package) error {
442-
defer debug.FunctionTimer().End()
443-
defer trace.StartRegion(ctx, "addPackagesToProfile").End()
444-
445-
if len(pkgs) == 0 {
446-
return nil
447-
}
448-
449-
// If packages are in profile but nixpkgs has been purged, the experience
450-
// will be poor when we try to run print-dev-env. So we ensure nixpkgs is
451-
// prefetched for all relevant packages (those not in binary cache).
452-
if err := devpkg.EnsureNixpkgsPrefetched(ctx, d.stderr, pkgs); err != nil {
453-
return err
454-
}
455-
456-
var msg string
457-
if len(pkgs) == 1 {
458-
msg = fmt.Sprintf("Installing package: %s.", pkgs[0])
459-
} else {
460-
pkgNames := lo.Map(pkgs, func(p *devpkg.Package, _ int) string { return p.Raw })
461-
msg = fmt.Sprintf("Installing %d packages: %s.", len(pkgs), strings.Join(pkgNames, ", "))
462-
}
463-
fmt.Fprintf(d.stderr, "\n%s\n\n", msg)
464-
465-
profileDir, err := d.profilePath()
466-
if err != nil {
467-
return fmt.Errorf("error getting profile path: %w", err)
468-
}
469-
470-
total := len(pkgs)
471-
for idx, pkg := range pkgs {
472-
stepNum := idx + 1
473-
474-
stepMsg := fmt.Sprintf("[%d/%d] %s", stepNum, total, pkg)
475-
476-
if err = nixprofile.ProfileInstall(ctx, &nixprofile.ProfileInstallArgs{
477-
CustomStepMessage: stepMsg,
478-
Lockfile: d.lockfile,
479-
Package: pkg.Raw,
480-
ProfilePath: profileDir,
481-
Writer: d.stderr,
482-
}); err != nil {
483-
return fmt.Errorf("error installing package %s: %w", pkg, err)
484-
}
485-
}
486-
487-
return nil
488-
}
489-
490337
var resetCheckDone = false
491338

492339
// resetProfileDirForFlakes ensures the profileDir directory is cleared of old

0 commit comments

Comments
 (0)