8
8
"fmt"
9
9
"io/fs"
10
10
"os"
11
- "os/exec"
12
11
"path/filepath"
13
12
"runtime/trace"
14
13
"slices"
@@ -188,18 +187,11 @@ func (d *Devbox) Remove(ctx context.Context, pkgs ...string) error {
188
187
return err
189
188
}
190
189
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
195
191
if err := d .ensurePackagesAreInstalled (ctx , uninstall ); err != nil {
196
192
return err
197
193
}
198
194
199
- if err := d .lockfile .Remove (packagesToUninstall ... ); err != nil {
200
- return err
201
- }
202
-
203
195
return d .saveCfg ()
204
196
}
205
197
@@ -215,15 +207,19 @@ const (
215
207
// ensurePackagesAreInstalled ensures that the nix profile has the packages specified
216
208
// in the config (devbox.json). The `mode` is used for user messaging to explain
217
209
// 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.
218
212
func (d * Devbox ) ensurePackagesAreInstalled (ctx context.Context , mode installMode ) error {
219
213
defer trace .StartRegion (ctx , "ensurePackages" ).End ()
220
214
defer debug .FunctionTimer ().End ()
221
215
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.
226
218
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
+ }
227
223
fmt .Fprintln (d .stderr , "Ensuring packages are installed." )
228
224
}
229
225
@@ -294,29 +290,97 @@ func (d *Devbox) profilePath() (string, error) {
294
290
// and no more.
295
291
func (d * Devbox ) syncPackagesToProfile (ctx context.Context , mode installMode ) error {
296
292
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 {
300
307
return err
301
308
}
302
309
303
- return d .tidyProfile (ctx )
304
- }
310
+ if err := devpkg .FillNarInfoCache (ctx , packages ... ); err != nil {
311
+ return err
312
+ }
305
313
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
+ }
311
319
320
+ // we are done if mode is uninstall
312
321
if mode == uninstall {
313
322
return nil
314
323
}
315
324
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 )
319
370
}
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 ()
320
384
321
385
if len (pkgs ) == 0 {
322
386
return nil
@@ -363,109 +427,6 @@ func (d *Devbox) addPackagesToProfile(ctx context.Context, mode installMode) err
363
427
return nil
364
428
}
365
429
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
-
469
430
// extraPackagesInProfile returns a list of packages that are in the nix profile,
470
431
// but are NOT in devbox.json or global devbox.json.
471
432
//
0 commit comments