Skip to content

Commit 754575d

Browse files
committed
[RFC] [refactor] Consolidate profile update code under syncPackagesToProfile
1 parent a3bac39 commit 754575d

File tree

2 files changed

+90
-134
lines changed

2 files changed

+90
-134
lines changed

internal/impl/packages.go

Lines changed: 90 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"io/fs"
1010
"os"
11-
"os/exec"
1211
"path/filepath"
1312
"runtime/trace"
1413
"slices"
@@ -188,18 +187,11 @@ func (d *Devbox) Remove(ctx context.Context, pkgs ...string) error {
188187
return err
189188
}
190189

191-
if err := d.removePackagesFromProfile(ctx, packagesToUninstall); err != nil {
192-
return err
193-
}
194-
190+
// this will clean up the now-extra package from nix profile and the lockfile
195191
if err := d.ensurePackagesAreInstalled(ctx, uninstall); err != nil {
196192
return err
197193
}
198194

199-
if err := d.lockfile.Remove(packagesToUninstall...); err != nil {
200-
return err
201-
}
202-
203195
return d.saveCfg()
204196
}
205197

@@ -215,15 +207,19 @@ const (
215207
// ensurePackagesAreInstalled ensures that the nix profile has the packages specified
216208
// in the config (devbox.json). The `mode` is used for user messaging to explain
217209
// what operations are happening, because this function may take time to execute.
210+
// TODO we should rename this to ensureDevboxEnvironmentIsUpToDate since it does
211+
// much more than ensuring packages are installed.
218212
func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMode) error {
219213
defer trace.StartRegion(ctx, "ensurePackages").End()
220214
defer debug.FunctionTimer().End()
221215

222-
if upToDate, err := d.lockfile.IsUpToDateAndInstalled(); err != nil || upToDate {
223-
return err
224-
}
225-
216+
// if mode is install or uninstall, then we need to update the nix-profile
217+
// and lockfile, so we must continue below.
226218
if mode == ensure {
219+
// if mode is ensure, then we only continue if needed.
220+
if upToDate, err := d.lockfile.IsUpToDateAndInstalled(); err != nil || upToDate {
221+
return err
222+
}
227223
fmt.Fprintln(d.stderr, "Ensuring packages are installed.")
228224
}
229225

@@ -294,29 +290,97 @@ func (d *Devbox) profilePath() (string, error) {
294290
// and no more.
295291
func (d *Devbox) syncPackagesToProfile(ctx context.Context, mode installMode) error {
296292
defer debug.FunctionTimer().End()
297-
// TODO: we can probably merge these two operations to be faster and minimize chances of
298-
// the devbox.json and nix profile falling out of sync.
299-
if err := d.addPackagesToProfile(ctx, mode); err != nil {
293+
defer trace.StartRegion(ctx, "syncPackagesToProfile").End()
294+
295+
// First, fetch the profile items from the nix-profile,
296+
// and get the installable packages
297+
profileDir, err := d.profilePath()
298+
if err != nil {
299+
return err
300+
}
301+
profileItems, err := nixprofile.ProfileListItems(d.stderr, profileDir)
302+
if err != nil {
303+
return err
304+
}
305+
packages, err := d.AllInstallablePackages()
306+
if err != nil {
300307
return err
301308
}
302309

303-
return d.tidyProfile(ctx)
304-
}
310+
if err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {
311+
return err
312+
}
305313

306-
// addPackagesToProfile inspects the packages in devbox.json, checks which of them
307-
// are missing from the nix profile, and then installs each package individually into the
308-
// nix profile.
309-
func (d *Devbox) addPackagesToProfile(ctx context.Context, mode installMode) error {
310-
defer trace.StartRegion(ctx, "addNixProfilePkgs").End()
314+
// Second, remove any packages from the nix-profile that are not in the config
315+
itemsToKeep, err := d.removeExtraItemsFromProfile(ctx, profileDir, profileItems, packages)
316+
if err != nil {
317+
return err
318+
}
311319

320+
// we are done if mode is uninstall
312321
if mode == uninstall {
313322
return nil
314323
}
315324

316-
pkgs, err := d.pendingPackagesForInstallation(ctx)
317-
if err != nil {
318-
return err
325+
// Last, find the pending packages, and ensure they are added to the nix-profile
326+
// Important to maintain the order of packages as specified by
327+
// Devbox.InstallablePackages() (higher priority first)
328+
pending := []*devpkg.Package{}
329+
for _, pkg := range packages {
330+
_, err := nixprofile.ProfileListIndex(&nixprofile.ProfileListIndexArgs{
331+
Items: itemsToKeep,
332+
Lockfile: d.lockfile,
333+
Writer: d.stderr,
334+
Package: pkg,
335+
ProfileDir: profileDir,
336+
})
337+
if err != nil {
338+
if !errors.Is(err, nix.ErrPackageNotFound) {
339+
return err
340+
}
341+
pending = append(pending, pkg)
342+
}
343+
}
344+
345+
return d.addPackagesToProfile(ctx, pending)
346+
}
347+
348+
func (d *Devbox) removeExtraItemsFromProfile(
349+
ctx context.Context,
350+
profileDir string,
351+
profileItems []*nixprofile.NixProfileListItem,
352+
packages []*devpkg.Package,
353+
) ([]*nixprofile.NixProfileListItem, error) {
354+
defer debug.FunctionTimer().End()
355+
defer trace.StartRegion(ctx, "removeExtraPackagesFromProfile").End()
356+
357+
itemsToKeep := []*nixprofile.NixProfileListItem{}
358+
extras := []*nixprofile.NixProfileListItem{}
359+
// Note: because devpkg.Package uses memoization when normalizing attribute paths (slow operation),
360+
// and since we're reusing the Package objects, this O(n*m) loop becomes O(n+m) wrt the slow operation.
361+
outer:
362+
for _, item := range profileItems {
363+
for _, pkg := range packages {
364+
if item.Matches(pkg, d.lockfile) {
365+
itemsToKeep = append(itemsToKeep, item)
366+
continue outer
367+
}
368+
}
369+
extras = append(extras, item)
319370
}
371+
// Remove by index to avoid comparing nix.ProfileListItem <> nix.Inputs again.
372+
if err := nixprofile.ProfileRemoveItems(profileDir, extras); err != nil {
373+
return nil, err
374+
}
375+
return itemsToKeep, nil
376+
}
377+
378+
// addPackagesToProfile inspects the packages in devbox.json, checks which of them
379+
// are missing from the nix profile, and then installs each package individually into the
380+
// nix profile.
381+
func (d *Devbox) addPackagesToProfile(ctx context.Context, pkgs []*devpkg.Package) error {
382+
defer debug.FunctionTimer().End()
383+
defer trace.StartRegion(ctx, "addPackagesToProfile").End()
320384

321385
if len(pkgs) == 0 {
322386
return nil
@@ -363,109 +427,6 @@ func (d *Devbox) addPackagesToProfile(ctx context.Context, mode installMode) err
363427
return nil
364428
}
365429

366-
func (d *Devbox) removePackagesFromProfile(ctx context.Context, pkgs []string) error {
367-
defer trace.StartRegion(ctx, "removeNixProfilePkgs").End()
368-
369-
profileDir, err := d.profilePath()
370-
if err != nil {
371-
return err
372-
}
373-
374-
for _, pkg := range devpkg.PackageFromStrings(pkgs, d.lockfile) {
375-
index, err := nixprofile.ProfileListIndex(&nixprofile.ProfileListIndexArgs{
376-
Lockfile: d.lockfile,
377-
Writer: d.stderr,
378-
Package: pkg,
379-
ProfileDir: profileDir,
380-
})
381-
if err != nil {
382-
debug.Log(
383-
"Info: Package %s not found in nix profile. Skipping removing from profile.\n",
384-
pkg.Raw,
385-
)
386-
continue
387-
}
388-
389-
// TODO: unify this with nix.ProfileRemove
390-
cmd := exec.Command("nix", "profile", "remove",
391-
"--profile", profileDir,
392-
fmt.Sprintf("%d", index),
393-
)
394-
cmd.Args = append(cmd.Args, nix.ExperimentalFlags()...)
395-
cmd.Stdout = d.stderr
396-
cmd.Stderr = d.stderr
397-
err = cmd.Run()
398-
if err != nil {
399-
return err
400-
}
401-
}
402-
return nil
403-
}
404-
405-
// tidyProfile removes any packages in the nix profile that are not in devbox.json.
406-
func (d *Devbox) tidyProfile(ctx context.Context) error {
407-
defer trace.StartRegion(ctx, "tidyProfile").End()
408-
409-
extras, err := d.extraPackagesInProfile(ctx)
410-
if err != nil {
411-
return err
412-
}
413-
414-
profileDir, err := d.profilePath()
415-
if err != nil {
416-
return err
417-
}
418-
419-
// Remove by index to avoid comparing nix.ProfileListItem <> nix.Inputs again.
420-
return nixprofile.ProfileRemoveItems(profileDir, extras)
421-
}
422-
423-
// pendingPackagesForInstallation returns a list of packages that are in
424-
// devbox.json or global devbox.json but are not yet installed in the nix
425-
// profile. It maintains the order of packages as specified by
426-
// Devbox.AllPackages() (higher priority first)
427-
func (d *Devbox) pendingPackagesForInstallation(ctx context.Context) ([]*devpkg.Package, error) {
428-
defer trace.StartRegion(ctx, "pendingPackages").End()
429-
430-
profileDir, err := d.profilePath()
431-
if err != nil {
432-
return nil, err
433-
}
434-
435-
pending := []*devpkg.Package{}
436-
items, err := nixprofile.ProfileListItems(d.stderr, profileDir)
437-
if err != nil {
438-
return nil, err
439-
}
440-
packages, err := d.AllInstallablePackages()
441-
if err != nil {
442-
return nil, err
443-
}
444-
445-
// Fill the narinfo cache for all packages so we can check if they are in the
446-
// binary cache.
447-
if err := devpkg.FillNarInfoCache(ctx, packages...); err != nil {
448-
return nil, err
449-
}
450-
451-
for _, pkg := range packages {
452-
_, err := nixprofile.ProfileListIndex(&nixprofile.ProfileListIndexArgs{
453-
Items: items,
454-
Lockfile: d.lockfile,
455-
Writer: d.stderr,
456-
Package: pkg,
457-
ProfileDir: profileDir,
458-
})
459-
if err != nil {
460-
if !errors.Is(err, nix.ErrPackageNotFound) {
461-
return nil, err
462-
}
463-
pending = append(pending, pkg)
464-
}
465-
}
466-
return pending, nil
467-
}
468-
469430
// extraPackagesInProfile returns a list of packages that are in the nix profile,
470431
// but are NOT in devbox.json or global devbox.json.
471432
//

internal/impl/update.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,6 @@ func (d *Devbox) mergeResolvedPackageToLockfile(
116116

117117
if existing.Version != resolved.Version {
118118
ux.Finfo(d.stderr, "Updating %s %s -> %s\n", pkg, existing.Version, resolved.Version)
119-
if err := d.removePackagesFromProfile(ctx, []string{pkg.Raw}); err != nil {
120-
// Warn but continue. TODO(landau): ensurePackagesAreInstalled should
121-
// sync the profile so we don't need to do this manually.
122-
ux.Fwarning(d.stderr, "Failed to remove %s from profile: %s\n", pkg, err)
123-
}
124119
resolved.AllowInsecure = existing.AllowInsecure
125120
lockfile.Packages[pkg.Raw] = resolved
126121
return nil

0 commit comments

Comments
 (0)