Skip to content

Commit 4259d40

Browse files
authored
Fix bug when comparing profile item added by store path (#1422)
## Summary When an package is added by store path, we can no longer recover the original package from the profile item itself, because the only information we have is the nix store path. So, some comparisons where we were trying to find if a profile item matched a devbox package were broken. Here we introduce a `Item.Matches(Package)` function to return true if the item was installed from the package. It attempts to hide the complexity of whether the item was installed by store path or not inside it, so callers don't have to worry about it. Modified the callsites to `ToPackage()` to use `Matches` instead, which simplified some logic. Also added a `len == 0` check to calls to `nix profile remove`, since I noticed we were sometimes calling it with no arguments. No failures here, but just wasted time. ## How was it tested? Manually tested by adding/removing packages and logging all the profile-related calls and item/package comparisons. --------- Signed-off-by: Rodrigo Ipince <[email protected]>
1 parent 945f5d8 commit 4259d40

File tree

4 files changed

+52
-50
lines changed

4 files changed

+52
-50
lines changed

internal/devpkg/package.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ func (p *Package) LegacyToVersioned() string {
424424
return p.Raw + "@latest"
425425
}
426426

427-
// ensureNixpkgsPrefetched will prefetch flake for the nixpkgs registry for the package.
427+
// EnsureNixpkgsPrefetched will prefetch flake for the nixpkgs registry for the package.
428428
// This is an internal method, and should not be called directly.
429429
func EnsureNixpkgsPrefetched(ctx context.Context, w io.Writer, pkgs []*Package) error {
430430
if err := FillNarInfoCache(ctx, pkgs...); err != nil {

internal/impl/packages.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -461,12 +461,12 @@ func (d *Devbox) extraPackagesInProfile(ctx context.Context) ([]*nixprofile.NixP
461461
if err != nil {
462462
return nil, err
463463
}
464-
devboxInputs, err := d.AllInstallablePackages()
464+
packages, err := d.AllInstallablePackages()
465465
if err != nil {
466466
return nil, err
467467
}
468468

469-
if len(devboxInputs) == len(profileItems) {
469+
if len(packages) == len(profileItems) {
470470
// Optimization: skip comparison if number of packages are the same. This only works
471471
// because we assume that all packages in `devbox.json` have just been added to the
472472
// profile.
@@ -478,9 +478,8 @@ func (d *Devbox) extraPackagesInProfile(ctx context.Context) ([]*nixprofile.NixP
478478
// and since we're reusing the Input objects, this O(n*m) loop becomes O(n+m) wrt the slow operation.
479479
outer:
480480
for _, item := range profileItems {
481-
profileInput := item.ToPackage(d.lockfile)
482-
for _, devboxInput := range devboxInputs {
483-
if profileInput.Equals(devboxInput) {
481+
for _, pkg := range packages {
482+
if item.Matches(pkg, d.lockfile) {
484483
continue outer
485484
}
486485
}

internal/nix/nixprofile/item.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ type NixProfileListItem struct {
1717
index int
1818

1919
// The original ("unlocked") flake reference and output attribute path used at installation time.
20-
// NOTE that this can be "#" if the package was added to the nix profile via store path.
20+
// NOTE that this will be empty if the package was added to the nix profile via store path.
2121
unlockedReference string
2222

2323
// The locked flake reference to which the unlocked flake reference was resolved.
24-
// NOTE that this can be "#" if the package was added to the nix profile via store path.
24+
// NOTE that this will be empty if the package was added to the nix profile via store path.
2525
lockedReference string
2626

27-
// The store path(s) of the package.
27+
// The store path(s) of the package. Should have at least 1 path, and should have exactly 1 path
28+
// if the item was added to the profile through a store path.
2829
nixStorePaths []string
2930
}
3031

@@ -33,35 +34,52 @@ type NixProfileListItem struct {
3334
// For example:
3435
// if NixProfileListItem.lockedReference = github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19
3536
// then AttributePath = legacyPackages.x86_64-darwin.go_1_19
36-
func (item *NixProfileListItem) AttributePath() (string, error) {
37+
func (i *NixProfileListItem) AttributePath() (string, error) {
3738
// lockedReference example:
3839
// github:NixOS/nixpkgs/52e3e80afff4b16ccb7c52e9f0f5220552f03d04#legacyPackages.x86_64-darwin.go_1_19
3940

4041
// AttributePath example:
4142
// legacyPackages.x86_64.go_1_19
42-
_ /*nixpkgs*/, attrPath, found := strings.Cut(item.lockedReference, "#")
43+
_ /*nixpkgs*/, attrPath, found := strings.Cut(i.lockedReference, "#")
4344
if !found {
4445
return "", redact.Errorf(
4546
"expected to find # in lockedReference: %s from NixProfileListItem: %s",
46-
redact.Safe(item.lockedReference),
47-
item,
47+
redact.Safe(i.lockedReference),
48+
i,
4849
)
4950
}
5051
return attrPath, nil
5152
}
5253

53-
// ToPackage constructs a nix.Package using the unlocked reference.
54-
// TODO: this doesn't handle well the case where item.unlockedReference == "#"
55-
func (item *NixProfileListItem) ToPackage(locker lock.Locker) *devpkg.Package {
56-
return devpkg.PackageFromString(item.unlockedReference, locker)
54+
// Matches compares a devpkg.Package with this profile item and returns true if the profile item
55+
// was the result of adding the Package to the nix profile.
56+
func (i *NixProfileListItem) Matches(pkg *devpkg.Package, locker lock.Locker) bool {
57+
if i.addedByStorePath() {
58+
// If an Item was added via store path, the best we can do when comparing to a Package is to check
59+
// if its store path matches that of the Package. Note that the item should only have 1 store path.
60+
path, err := pkg.InputAddressedPath()
61+
if err != nil {
62+
// pkg couldn't have been added by store path if we can't get the store path for it, so return
63+
// false. There are some edge cases (e.g. cache is down, index changed, etc., but it's OK to
64+
// err on the side of false).
65+
return false
66+
}
67+
return len(i.nixStorePaths) == 1 && i.nixStorePaths[0] == path
68+
}
69+
70+
return pkg.Equals(devpkg.PackageFromString(i.unlockedReference, locker))
71+
}
72+
73+
func (i *NixProfileListItem) addedByStorePath() bool {
74+
return i.unlockedReference == ""
5775
}
5876

5977
// String serializes the NixProfileListItem back into the format printed by `nix profile list`
60-
func (item *NixProfileListItem) String() string {
61-
return fmt.Sprintf("%d %s %s %s",
62-
item.index,
63-
item.unlockedReference,
64-
item.lockedReference,
65-
item.nixStorePaths,
78+
func (i *NixProfileListItem) String() string {
79+
return fmt.Sprintf("{%d %s %s %s}",
80+
i.index,
81+
i.unlockedReference,
82+
i.lockedReference,
83+
i.nixStorePaths,
6684
)
6785
}

internal/nix/nixprofile/profile.go

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/fatih/color"
1616
"github.com/pkg/errors"
17+
"github.com/samber/lo"
1718
"go.jetpack.io/devbox/internal/boxcli/usererr"
1819
"go.jetpack.io/devbox/internal/devpkg"
1920
"go.jetpack.io/devbox/internal/lock"
@@ -56,8 +57,8 @@ func ProfileListItems(
5657
for index, element := range structOutput.Elements {
5758
items = append(items, &NixProfileListItem{
5859
index: index,
59-
unlockedReference: element.OriginalURL + "#" + element.AttrPath,
60-
lockedReference: element.URL + "#" + element.AttrPath,
60+
unlockedReference: lo.Ternary(element.OriginalURL != "", element.OriginalURL+"#"+element.AttrPath, ""),
61+
lockedReference: lo.Ternary(element.URL != "", element.URL+"#"+element.AttrPath, ""),
6162
nixStorePaths: element.StorePaths,
6263
})
6364
}
@@ -125,41 +126,25 @@ func ProfileListIndex(args *ProfileListIndexArgs) (int, error) {
125126
if err != nil {
126127
return -1, err
127128
}
128-
if inCache {
129-
// Packages in cache are added by store path, which means we only need to check
130-
// for store path equality to find it.
131-
pathInStore, err := args.Package.Installable()
129+
130+
if !inCache {
131+
// This is an optimization for happy path when packages are added by flake reference. A resolved devbox
132+
// package *which was added by flake reference* (not by store path) should match the unlockedReference
133+
// of an existing profile item.
134+
ref, err := args.Package.NormalizedDevboxPackageReference()
132135
if err != nil {
133136
return -1, errors.Wrapf(err, "failed to get installable for %s", args.Package.String())
134137
}
135138
for _, item := range items {
136-
if len(item.nixStorePaths) == 1 && // this should always be true
137-
pathInStore == item.nixStorePaths[0] {
139+
if ref == item.unlockedReference {
138140
return item.index, nil
139141
}
140142
}
141143
return -1, errors.Wrap(nix.ErrPackageNotFound, args.Package.String())
142144
}
143145

144-
// else: check if the Package matches an item's unlockedReference.
145-
// This is an optimization for happy path. A resolved devbox package *which was added by
146-
// flake reference* (not by store path) should match the unlockedReference of an existing
147-
// profile item.
148-
ref, err := args.Package.NormalizedDevboxPackageReference()
149-
if err != nil {
150-
return -1, err
151-
}
152146
for _, item := range items {
153-
if ref == item.unlockedReference {
154-
return item.index, nil
155-
}
156-
}
157-
158-
// Still not found? Check for full pkg equality (may be expensive).
159-
for _, item := range items {
160-
existing := item.ToPackage(args.Lockfile)
161-
162-
if args.Package.Equals(existing) {
147+
if item.Matches(args.Package, args.Lockfile) {
163148
return item.index, nil
164149
}
165150
}
@@ -276,7 +261,7 @@ func ProfileInstall(ctx context.Context, args *ProfileInstallArgs) error {
276261
// It is up to the caller to ensure that the underlying profile has not changed since the items
277262
// were queried.
278263
func ProfileRemoveItems(profilePath string, items []*NixProfileListItem) error {
279-
if items == nil {
264+
if len(items) == 0 {
280265
return nil
281266
}
282267
indexes := []string{}

0 commit comments

Comments
 (0)