@@ -273,14 +273,14 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
273
273
274
274
recomputeState := mode == ensure || d .IsEnvEnabled ()
275
275
if recomputeState {
276
- if err := d .recomputeState (ctx , mode ); err != nil {
276
+ if err := d .recomputeState (ctx ); err != nil {
277
277
return err
278
278
}
279
279
} else {
280
- // TODO: in the next PR, we will only `nix build` the packages that are being
281
- // added or updated. For now, we continue to call recomputeState here.
282
- if err := d . recomputeState ( ctx , mode ); err != nil {
283
- return err
280
+ if mode == install || mode == update {
281
+ if err := d . installNixPackagesToStore ( ctx ); err != nil {
282
+ return err
283
+ }
284
284
}
285
285
}
286
286
@@ -312,18 +312,14 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
312
312
// - devbox.lock file
313
313
// - the generated flake
314
314
// - the nix-profile
315
- func (d * Devbox ) recomputeState (ctx context.Context , mode installMode ) error {
315
+ func (d * Devbox ) recomputeState (ctx context.Context ) error {
316
316
// Create plugin directories first because packages might need them
317
317
for _ , pkg := range d .InstallablePackages () {
318
318
if err := d .PluginManager ().Create (pkg ); err != nil {
319
319
return err
320
320
}
321
321
}
322
322
323
- if err := d .syncPackagesToProfile (ctx , mode ); err != nil {
324
- return err
325
- }
326
-
327
323
if err := d .InstallRunXPackages (ctx ); err != nil {
328
324
return err
329
325
}
@@ -336,10 +332,11 @@ func (d *Devbox) recomputeState(ctx context.Context, mode installMode) error {
336
332
return err
337
333
}
338
334
339
- // Use the printDevEnvCache if we are adding or removing or updating any package,
340
- // AND we are not in the shellenv-enabled environment of the current devbox-project.
341
- usePrintDevEnvCache := mode != ensure && ! d .IsEnvEnabled ()
342
- if _ , err := d .computeEnv (ctx , usePrintDevEnvCache ); err != nil {
335
+ if err := d .installNixPackagesToStore (ctx ); err != nil {
336
+ return err
337
+ }
338
+
339
+ if err := d .syncNixProfileFromFlake (ctx ); err != nil {
343
340
return err
344
341
}
345
342
@@ -366,162 +363,6 @@ func (d *Devbox) profilePath() (string, error) {
366
363
return absPath , errors .WithStack (os .MkdirAll (filepath .Dir (absPath ), 0o755 ))
367
364
}
368
365
369
- // syncPackagesToProfile can ensure that all packages in devbox.json exist in the nix profile,
370
- // and no more. However, it may skip some steps depending on the `mode`.
371
- func (d * Devbox ) syncPackagesToProfile (ctx context.Context , mode installMode ) error {
372
- defer debug .FunctionTimer ().End ()
373
- defer trace .StartRegion (ctx , "syncPackagesToProfile" ).End ()
374
-
375
- // First, fetch the profile items from the nix-profile,
376
- // and get the installable packages
377
- profileDir , err := d .profilePath ()
378
- if err != nil {
379
- return err
380
- }
381
- profileItems , err := nixprofile .ProfileListItems (d .stderr , profileDir )
382
- if err != nil {
383
- return err
384
- }
385
- packages , err := d .AllInstallablePackages ()
386
- if err != nil {
387
- return err
388
- }
389
-
390
- // Remove non-nix packages from the list
391
- packages = lo .Filter (packages , devpkg .IsNix )
392
-
393
- if err := devpkg .FillNarInfoCache (ctx , packages ... ); err != nil {
394
- return err
395
- }
396
-
397
- // Second, remove any packages from the nix-profile that are not in the config
398
- itemsToKeep := profileItems
399
- if mode != install {
400
- itemsToKeep , err = d .removeExtraItemsFromProfile (ctx , profileDir , profileItems , packages )
401
- if err != nil {
402
- return err
403
- }
404
- }
405
-
406
- // we are done if mode is uninstall
407
- if mode == uninstall {
408
- return nil
409
- }
410
-
411
- // Last, find the pending packages, and ensure they are added to the nix-profile
412
- // Important to maintain the order of packages as specified by
413
- // Devbox.InstallablePackages() (higher priority first).
414
- // We also run nix profile upgrade on any virtenv flakes. This is a bit of a
415
- // blunt approach, but ensured any plugin created flakes are up-to-date.
416
- pending := []* devpkg.Package {}
417
- for _ , pkg := range packages {
418
- idx , err := nixprofile .ProfileListIndex (& nixprofile.ProfileListIndexArgs {
419
- Items : itemsToKeep ,
420
- Lockfile : d .lockfile ,
421
- Writer : d .stderr ,
422
- Package : pkg ,
423
- ProfileDir : profileDir ,
424
- })
425
- if err != nil {
426
- if ! errors .Is (err , nix .ErrPackageNotFound ) {
427
- return err
428
- }
429
- pending = append (pending , pkg )
430
- } else if f , err := pkg .FlakeInstallable (); err == nil && d .pluginManager .PathIsInVirtenv (f .Ref .Path ) {
431
- if err := nix .ProfileUpgrade (profileDir , idx ); err != nil {
432
- return err
433
- }
434
- }
435
- }
436
-
437
- return d .addPackagesToProfile (ctx , pending )
438
- }
439
-
440
- func (d * Devbox ) removeExtraItemsFromProfile (
441
- ctx context.Context ,
442
- profileDir string ,
443
- profileItems []* nixprofile.NixProfileListItem ,
444
- packages []* devpkg.Package ,
445
- ) ([]* nixprofile.NixProfileListItem , error ) {
446
- defer debug .FunctionTimer ().End ()
447
- defer trace .StartRegion (ctx , "removeExtraPackagesFromProfile" ).End ()
448
-
449
- itemsToKeep := []* nixprofile.NixProfileListItem {}
450
- extras := []* nixprofile.NixProfileListItem {}
451
- // Note: because devpkg.Package uses memoization when normalizing attribute paths (slow operation),
452
- // and since we're reusing the Package objects, this O(n*m) loop becomes O(n+m) wrt the slow operation.
453
- for _ , item := range profileItems {
454
- found := false
455
- for _ , pkg := range packages {
456
- if item .Matches (pkg , d .lockfile ) {
457
- itemsToKeep = append (itemsToKeep , item )
458
- found = true
459
- break
460
- }
461
- }
462
- if ! found {
463
- extras = append (extras , item )
464
- }
465
- }
466
- // Remove by index to avoid comparing nix.ProfileListItem <> nix.Inputs again.
467
- if err := nixprofile .ProfileRemoveItems (profileDir , extras ); err != nil {
468
- return nil , err
469
- }
470
- return itemsToKeep , nil
471
- }
472
-
473
- // addPackagesToProfile inspects the packages in devbox.json, checks which of them
474
- // are missing from the nix profile, and then installs each package individually into the
475
- // nix profile.
476
- func (d * Devbox ) addPackagesToProfile (ctx context.Context , pkgs []* devpkg.Package ) error {
477
- defer debug .FunctionTimer ().End ()
478
- defer trace .StartRegion (ctx , "addPackagesToProfile" ).End ()
479
-
480
- if len (pkgs ) == 0 {
481
- return nil
482
- }
483
-
484
- // If packages are in profile but nixpkgs has been purged, the experience
485
- // will be poor when we try to run print-dev-env. So we ensure nixpkgs is
486
- // prefetched for all relevant packages (those not in binary cache).
487
- if err := devpkg .EnsureNixpkgsPrefetched (ctx , d .stderr , pkgs ); err != nil {
488
- return err
489
- }
490
-
491
- var msg string
492
- if len (pkgs ) == 1 {
493
- msg = fmt .Sprintf ("Installing package: %s." , pkgs [0 ])
494
- } else {
495
- pkgNames := lo .Map (pkgs , func (p * devpkg.Package , _ int ) string { return p .Raw })
496
- msg = fmt .Sprintf ("Installing %d packages: %s." , len (pkgs ), strings .Join (pkgNames , ", " ))
497
- }
498
- fmt .Fprintf (d .stderr , "\n %s\n \n " , msg )
499
-
500
- profileDir , err := d .profilePath ()
501
- if err != nil {
502
- return fmt .Errorf ("error getting profile path: %w" , err )
503
- }
504
-
505
- total := len (pkgs )
506
- for idx , pkg := range pkgs {
507
- stepNum := idx + 1
508
-
509
- stepMsg := fmt .Sprintf ("[%d/%d] %s" , stepNum , total , pkg )
510
-
511
- if err = nixprofile .ProfileInstall (ctx , & nixprofile.ProfileInstallArgs {
512
- CustomStepMessage : stepMsg ,
513
- Lockfile : d .lockfile ,
514
- Package : pkg .Raw ,
515
- ProfilePath : profileDir ,
516
- Writer : d .stderr ,
517
- }); err != nil {
518
- return fmt .Errorf ("error installing package %s: %w" , pkg , err )
519
- }
520
- }
521
-
522
- return nil
523
- }
524
-
525
366
var resetCheckDone = false
526
367
527
368
// resetProfileDirForFlakes ensures the profileDir directory is cleared of old
@@ -571,3 +412,80 @@ func (d *Devbox) InstallRunXPackages(ctx context.Context) error {
571
412
}
572
413
return nil
573
414
}
415
+
416
+ // installNixPackagesToStore will install all the packages in the nix store, if
417
+ // mode is install or update, and we're not in a devbox environment.
418
+ // This is done by running `nix build` on the flake. We do this so that the
419
+ // packages will be available in the nix store when computing the devbox environment
420
+ // and installing in the nix profile (even if offline).
421
+ func (d * Devbox ) installNixPackagesToStore (ctx context.Context ) error {
422
+ packages , err := d .packagesToInstallInProfile (ctx )
423
+ if err != nil {
424
+ return err
425
+ }
426
+
427
+ for _ , pkg := range packages {
428
+ installable , err := pkg .Installable ()
429
+ if err != nil {
430
+ return err
431
+ }
432
+
433
+ ux .Finfo (d .stderr , "Installing: %s\n " , pkg .Raw )
434
+ // --no-link to avoid generating the result objects
435
+ err = nix .Build (ctx , []string {"--no-link" }, installable )
436
+ if err != nil {
437
+ platform := nix .System ()
438
+ return usererr .New (
439
+ "package %s cannot be installed on your platform %s.\n " +
440
+ "If you know this package is incompatible with %[2]s, then " +
441
+ "you could run `devbox add %[1]s --exclude-platform %[2]s` and re-try.\n " +
442
+ "If you think this package should be compatible with %[2]s, then " +
443
+ "it's possible this particular version is not available yet from the nix registry. " +
444
+ "You could try `devbox add` with a different version for this package.\n " ,
445
+ pkg .Raw ,
446
+ platform ,
447
+ )
448
+ }
449
+ }
450
+ return err
451
+ }
452
+
453
+ func (d * Devbox ) packagesToInstallInProfile (ctx context.Context ) ([]* devpkg.Package , error ) {
454
+ // First, fetch the profile items from the nix-profile,
455
+ profileDir , err := d .profilePath ()
456
+ if err != nil {
457
+ return nil , err
458
+ }
459
+ profileItems , err := nixprofile .ProfileListItems (d .stderr , profileDir )
460
+ if err != nil {
461
+ return nil , err
462
+ }
463
+
464
+ // Second, get and prepare all the packages that must be installed in this project
465
+ packages , err := d .AllInstallablePackages ()
466
+ if err != nil {
467
+ return nil , err
468
+ }
469
+ packages = lo .Filter (packages , devpkg .IsNix ) // Remove non-nix packages from the list
470
+ if err := devpkg .FillNarInfoCache (ctx , packages ... ); err != nil {
471
+ return nil , err
472
+ }
473
+
474
+ // Third, compute which packages need to be installed
475
+ packagesToInstall := []* devpkg.Package {}
476
+ // Note: because devpkg.Package uses memoization when normalizing attribute paths (slow operation),
477
+ // and since we're reusing the Package objects, this O(n*m) loop becomes O(n+m) wrt the slow operation.
478
+ for _ , pkg := range packages {
479
+ found := false
480
+ for _ , item := range profileItems {
481
+ if item .Matches (pkg , d .lockfile ) {
482
+ found = true
483
+ break
484
+ }
485
+ }
486
+ if ! found {
487
+ packagesToInstall = append (packagesToInstall , pkg )
488
+ }
489
+ }
490
+ return packagesToInstall , nil
491
+ }
0 commit comments