Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Outputs] Add store-paths of package outputs to devbox.lock #1814

Merged
merged 4 commits into from
Feb 21, 2024

Conversation

savil
Copy link
Collaborator

@savil savil commented Feb 14, 2024

Summary

This PR updates the devbox.lock file to incorporate the expanded response from /v2/resolve api. This /v2/response incorporates store-paths for each output of the nix package.

To do so, we update lock.SystemInfo to expect []Output instead of the previous StorePath string. This SystemInfo struct needs to be backwards-compatible to read existing devbox.lock files that have the StorePath string. So, the read-path in lock.GetFile has some fallback code to handle this scenario, where the StorePath is added as Output{ Name: "out", path: <path>, Default: true}.

The main challenge with writing this PR was that we don't want to update devbox.lock unless there's been an explicit user action of the form devbox update or devbox add or devbox remove. Unfortunately, the lock.File gets internally updated by various code paths using its file.Resolve() function, and so it was (previously) hard for the lockfile
to track when an add/remove/update action happened.

To work around this, I have more explicitly invoked lockfile.Add and lockfile.Remove in the "add package" and "remove package" code paths. I think this is safe to do, but let me know if you feel a better path exists. This lets us track whether to write the lockfile with the legacy format of lock.SystemInfo (i.e. Store Path) or the modern format (i.e. []Output).

How was it tested?

  1. devbox update in devbox-repo doesn't have changes so the lock file wasn't changed.
  2. rm -rf .devbox && devbox shell. The lockfile wasn't changed.
  3. devbox add prometheus added the package in the modern []Output format of SystemInfo, and also updated the other packages to use the modern SystemInfo format.

Copy link
Collaborator Author

savil commented Feb 14, 2024

Current dependencies on/for this PR:

This stack of pull requests is managed by Graphite.

@savil savil force-pushed the savil/outputs-lockfile branch 6 times, most recently from bdba596 to 7718ef5 Compare February 15, 2024 00:57
@savil savil requested review from mikeland73 and gcurtis and removed request for mikeland73 February 15, 2024 02:00
@savil savil marked this pull request as ready for review February 15, 2024 02:00
Comment on lines 110 to 112
if err := d.lockfile.Add(addedPackageNames...); err != nil {
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Main issue with this is that this saves the lock file right away. This will leave it in a bad state if something fails later on. It's also not 100% correct (see newFlakePlan calls Add)

A few alternatives:

  • Instead of doing lockfile.Add just do lockfile.UpdateSystemsFormat() in the 3 places you need it (add/remove/update). This would update the internal boolean. This is simplest. We can keep this function around whenever we want to update lock file formats.
  • In lockfile.Save() only update the format is stuff is dirty (i.e if reloading the lockfile doesn't match what is in memory). (though it may be a bit complex to tell if dirty). This is cleanest solution because it doesn't require a new bool we may forget to add.
  • Remove Save from Add (this is probably OK, but it feels risky). I don't love this one.

FWIW, currently newFlakePlan calls lockfile.Add on includes every time the state needs to be recomputed, so I think any project with includes will automatically update.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ugh, I thought I had checked for existing callers, but I think I missed this one...good catch

Copy link
Contributor

@mikeland73 mikeland73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments with alternative approach. There's a minor bug because of previous use of Add

I think the cleanest solution that would not require boolean:

  • If lockfile contains store paths it is legacy.
  • If lockfile is dirty (i.e. loaded version doesn't match file) we modernize. (small perf hit for re-reading file, but we should rarely write to lock file and when we do, it's a slower operation anyway)

But if you run into issues implementing this, I think using a boolean is fine.

internal/lock/lockfile.go Outdated Show resolved Hide resolved
internal/lock/lockfile.go Outdated Show resolved Hide resolved
if o == nil || other == nil {
return o == other
}
return o.Name == other.Name && o.Path == other.Path && o.Default == other.Default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is normal equality in golang. You can do *o == *other

// It is the cache key in the Binary Cache Store (cache.nixos.org)
// It is of the form /nix/store/<hash>-<name>-<version>
// <name> may be different from the canonicalName so we store the full store path.
Outputs []*Output `json:"outputs,omitempty"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why pointers? If you make them structs you don't need the Equals function below you can just compare

a == b

Comment on lines 91 to 93
isEqual := true
if len(i.Outputs) != len(other.Outputs) {
return false
}
for i, o := range i.Outputs {
if !o.Equals(other.Outputs[i]) {
return false
}
}
return isEqual
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slices.Compare == 0 (assuming you don't use pointers, see comment above)

@savil
Copy link
Collaborator Author

savil commented Feb 15, 2024

If lockfile contains store paths it is legacy.

This doesn't work in the following scenario:

  1. user reads legacy devbox.lock so lockfile has non-empty StorePath
  2. user does devbox remove
  3. During lockfile.Save, the lockfile will have the legacy StorePath so we'll save it in legacy format
    BUT since the user did an explicit action that updates devbox.lock, we want to actually also write it in modern format.

If lockfile is dirty (i.e. loaded version doesn't match file) we modernize. (small perf hit for re-reading file, but we should rarely write to lock file and when we do, it's a slower operation anyway)

yay! this works :)
Updated code.

Copy link
Collaborator

@gcurtis gcurtis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using "out" for legacy data is okay. I'm pretty sure the original store_path JSON field mapped to .outPath attribute of the package.

@@ -43,9 +64,50 @@ func (p *Package) IsAllowInsecure() bool {
return p.AllowInsecure
}

// Useful for debugging when we print the struct
func (i *SystemInfo) String() string {
return fmt.Sprintf("SystemInfo{Outputs:%v, StorePath:%s}", i.Outputs, i.StorePath)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Sprintf("SystemInfo{Outputs:%v, StorePath:%s}", i.Outputs, i.StorePath)
return fmt.Sprintf("%+v", *i)

func (i *SystemInfo) Equals(other *SystemInfo) bool {
if i == nil || other == nil {
return i == other
}
return *i == *other

return slices.Equal(i.Outputs, other.Outputs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to take i.StorePath into account?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK? at least looking at the callsite https://github.com/jetpack-io/devbox/blob/main/internal/devbox/update.go#L142-L146

Only outputs matter moving forward and storePath is just kept for legacy reasons (to avoid updating everyone's lockfile)

func (f *File) Save() error {
isDirty, err := f.isDirty()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sweet! forgot we had this.

func (i *SystemInfo) Equals(other *SystemInfo) bool {
if i == nil || other == nil {
return i == other
}
return *i == *other

return slices.Equal(i.Outputs, other.Outputs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is OK? at least looking at the callsite https://github.com/jetpack-io/devbox/blob/main/internal/devbox/update.go#L142-L146

Only outputs matter moving forward and storePath is just kept for legacy reasons (to avoid updating everyone's lockfile)

@savil savil merged commit 37de8f0 into main Feb 21, 2024
25 checks passed
@savil savil deleted the savil/outputs-lockfile branch February 21, 2024 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants