Skip to content

Commit b56b7f1

Browse files
authored
[cache] Optimize cache upload (#2083)
## Summary This changes `devbox cache upload` so that it now longer has to build packages if they are already in cache. For each package, it first checks if it is in target cache. If it is, it skips. Otherwise it builds/copies. ## How was it tested? <img width="843" alt="image" src="https://github.com/jetify-com/devbox/assets/544948/225b744a-ff20-446e-a7d0-6436005a33ac">
1 parent ba5d4ee commit b56b7f1

File tree

4 files changed

+143
-83
lines changed

4 files changed

+143
-83
lines changed

internal/devbox/cache.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import (
55
"errors"
66
"io"
77

8+
"github.com/samber/lo"
89
"go.jetpack.io/devbox/internal/boxcli/usererr"
910
"go.jetpack.io/devbox/internal/build"
11+
"go.jetpack.io/devbox/internal/debug"
1012
"go.jetpack.io/devbox/internal/devbox/providers/identity"
1113
"go.jetpack.io/devbox/internal/devbox/providers/nixcache"
14+
"go.jetpack.io/devbox/internal/devpkg"
1215
"go.jetpack.io/devbox/internal/nix"
1316
"go.jetpack.io/devbox/internal/ux"
1417
"go.jetpack.io/pkg/auth"
@@ -18,6 +21,7 @@ func (d *Devbox) UploadProjectToCache(
1821
ctx context.Context,
1922
cacheURI string,
2023
) error {
24+
defer debug.FunctionTimer().End()
2125
if cacheURI == "" {
2226
var err error
2327
cacheURI, err = getWriteCacheURI(ctx, d.stderr)
@@ -30,19 +34,35 @@ func (d *Devbox) UploadProjectToCache(
3034
if err != nil && !errors.Is(err, auth.ErrNotLoggedIn) {
3135
return err
3236
}
33-
profilePath, err := d.profilePath()
34-
if err != nil {
37+
38+
packages := lo.Filter(d.InstallablePackages(), devpkg.IsNix)
39+
if err != nil || len(packages) == 0 {
3540
return err
3641
}
3742

38-
// Ensure state is up to date before uploading to cache.
39-
// TODO: we may be able to do this more efficiently, not sure everything needs
40-
// to be installed.
41-
if err = d.ensureStateIsUpToDate(ctx, ensure); err != nil {
42-
return err
43+
for _, pkg := range packages {
44+
inCache, err := pkg.AreAllOutputsInCache(ctx, d.stderr, cacheURI)
45+
if err != nil {
46+
return err
47+
}
48+
if inCache {
49+
ux.Finfo(d.stderr, "Package %s is already in cache, skipping\n", pkg.Raw)
50+
continue
51+
}
52+
ux.Finfo(d.stderr, "Uploading package %s to cache\n", pkg.Raw)
53+
installables, err := pkg.Installables()
54+
if err != nil {
55+
return err
56+
}
57+
for _, installable := range installables {
58+
err := nix.CopyInstallableToCache(ctx, d.stderr, cacheURI, installable, creds.Env())
59+
if err != nil {
60+
return err
61+
}
62+
}
4363
}
4464

45-
return nix.CopyInstallableToCache(ctx, d.stderr, cacheURI, profilePath, creds.Env())
65+
return nil
4666
}
4767

4868
func UploadInstallableToCache(

internal/devbox/packages.go

Lines changed: 6 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -553,32 +553,15 @@ func (d *Devbox) packagesToInstallInStore(ctx context.Context, mode installMode)
553553
packagesToInstall := []*devpkg.Package{}
554554
storePathsForPackage := map[*devpkg.Package][]string{}
555555
for _, pkg := range packages {
556-
installables, err := pkg.Installables()
556+
if mode == update {
557+
packagesToInstall = append(packagesToInstall, pkg)
558+
continue
559+
}
560+
var err error
561+
storePathsForPackage[pkg], err = pkg.GetStorePaths(ctx, d.stderr)
557562
if err != nil {
558563
return nil, err
559564
}
560-
for _, installable := range installables {
561-
if mode == update {
562-
packagesToInstall = append(packagesToInstall, pkg)
563-
continue
564-
}
565-
566-
resolvedStorePaths, err := pkg.GetResolvedStorePaths()
567-
if err != nil {
568-
return nil, err
569-
}
570-
if len(resolvedStorePaths) > 0 {
571-
storePathsForPackage[pkg] = append(storePathsForPackage[pkg], resolvedStorePaths...)
572-
continue
573-
}
574-
575-
storePathsForInstallable, err := nix.StorePathsFromInstallable(
576-
ctx, installable, pkg.HasAllowInsecure())
577-
if err != nil {
578-
return nil, packageInstallErrorHandler(err, pkg, installable)
579-
}
580-
storePathsForPackage[pkg] = append(storePathsForPackage[pkg], storePathsForInstallable...)
581-
}
582565
}
583566

584567
// Batch this for perf
@@ -599,58 +582,6 @@ func (d *Devbox) packagesToInstallInStore(ctx context.Context, mode installMode)
599582
return lo.Uniq(packagesToInstall), nil
600583
}
601584

602-
// packageInstallErrorHandler checks for two kinds of errors to print custom messages for so that Devbox users
603-
// can work around them:
604-
// 1. Packages that cannot be installed on the current system, but may be installable on other systems.packageInstallErrorHandler
605-
// 2. Packages marked insecure by nix
606-
func packageInstallErrorHandler(err error, pkg *devpkg.Package, installableOrEmpty string) error {
607-
if err == nil {
608-
return nil
609-
}
610-
611-
// Check if the user is installing a package that cannot be installed on their platform.
612-
// For example, glibcLocales on MacOS will give the following error:
613-
// flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path
614-
// This is because glibcLocales is only available on Linux.
615-
// The user should try `devbox add` again with `--exclude-platform`
616-
errMessage := strings.TrimSpace(err.Error())
617-
618-
// Sample error from `devbox add glibcLocales` on a mac:
619-
// error: flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path
620-
maybePackageSystemCompatibilityErrorType1 := strings.Contains(errMessage, "error: flake output attribute") &&
621-
strings.Contains(errMessage, "is not a derivation or path")
622-
// Sample error from `devbox add sublime4` on a mac:
623-
// error: Package ‘sublimetext4-4169’ in /nix/store/nlbjx0mp83p2qzf1rkmzbgvq1wxfir81-source/pkgs/applications/editors/sublime/4/common.nix:168 is not available on the requested hostPlatform:
624-
// hostPlatform.config = "x86_64-apple-darwin"
625-
// package.meta.platforms = [
626-
// "aarch64-linux"
627-
// "x86_64-linux"
628-
// ]
629-
maybePackageSystemCompatibilityErrorType2 := strings.Contains(errMessage, "is not available on the requested hostPlatform")
630-
631-
if maybePackageSystemCompatibilityErrorType1 || maybePackageSystemCompatibilityErrorType2 {
632-
platform := nix.System()
633-
return usererr.WithUserMessage(
634-
err,
635-
"package %s cannot be installed on your platform %s.\n"+
636-
"If you know this package is incompatible with %[2]s, then "+
637-
"you could run `devbox add %[1]s --exclude-platform %[2]s` and re-try.\n"+
638-
"If you think this package should be compatible with %[2]s, then "+
639-
"it's possible this particular version is not available yet from the nix registry. "+
640-
"You could try `devbox add` with a different version for this package.\n\n"+
641-
"Underlying Error from nix is:",
642-
pkg.Versioned(),
643-
platform,
644-
)
645-
}
646-
647-
if isInsecureErr, userErr := nix.IsExitErrorInsecurePackage(err, pkg.Versioned(), installableOrEmpty); isInsecureErr {
648-
return userErr
649-
}
650-
651-
return usererr.WithUserMessage(err, "error installing package %s", pkg.Raw)
652-
}
653-
654585
// moveAllowInsecureFromLockfile will modernize a Devbox project by moving the allow_insecure: boolean
655586
// setting from the devbox.lock file to the corresponding package in devbox.json.
656587
//

internal/devpkg/narinfo_cache.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package devpkg
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"net/http"
78
"net/url"
89
"strings"
@@ -172,6 +173,32 @@ func (p *Package) fetchNarInfoStatusOnce(
172173
return outputToCache, nil
173174
}
174175

176+
func (p *Package) AreAllOutputsInCache(
177+
ctx context.Context, w io.Writer, cacheURI string,
178+
) (bool, error) {
179+
storePaths, err := p.GetStorePaths(ctx, w)
180+
if err != nil {
181+
return false, err
182+
}
183+
184+
for _, storePath := range storePaths {
185+
pathParts := nix.NewStorePathParts(storePath)
186+
hash := pathParts.Hash
187+
if strings.HasPrefix(cacheURI, "s3") {
188+
inCache, err := fetchNarInfoStatusFromS3(ctx, cacheURI, hash)
189+
if err != nil || !inCache {
190+
return false, err
191+
}
192+
} else {
193+
inCache, err := fetchNarInfoStatusFromHTTP(ctx, cacheURI, hash)
194+
if err != nil || !inCache {
195+
return false, err
196+
}
197+
}
198+
}
199+
return true, nil
200+
}
201+
175202
func (p *Package) outputsForOutputName(output string) ([]lock.Output, error) {
176203
sysInfo, err := p.sysInfoIfExists()
177204
if err != nil || sysInfo == nil {

internal/devpkg/package.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
2323
"go.jetpack.io/devbox/internal/lock"
2424
"go.jetpack.io/devbox/internal/nix"
25+
"go.jetpack.io/devbox/internal/ux"
2526
"go.jetpack.io/devbox/nix/flake"
2627
"go.jetpack.io/devbox/plugins"
2728
)
@@ -734,3 +735,84 @@ func (p *Package) GetResolvedStorePaths() ([]string, error) {
734735
}
735736
return storePaths, nil
736737
}
738+
739+
func (p *Package) GetStorePaths(ctx context.Context, w io.Writer) ([]string, error) {
740+
storePathsForPackage, err := p.GetResolvedStorePaths()
741+
if err != nil || len(storePathsForPackage) > 0 {
742+
return storePathsForPackage, err
743+
}
744+
745+
// No fast path, we need to query nix.
746+
// TODO we should give people the option to add paths to lockfile.
747+
ux.Fwarning(
748+
w,
749+
"Outputs for %s are not in lockfile. Fetching store paths from nix, this may take a while\n",
750+
p.Raw,
751+
)
752+
753+
installables, err := p.Installables()
754+
if err != nil {
755+
return nil, err
756+
}
757+
for _, installable := range installables {
758+
storePathsForInstallable, err := nix.StorePathsFromInstallable(
759+
ctx, installable, p.HasAllowInsecure())
760+
if err != nil {
761+
return nil, packageInstallErrorHandler(err, p, installable)
762+
}
763+
storePathsForPackage = append(storePathsForPackage, storePathsForInstallable...)
764+
}
765+
return storePathsForPackage, nil
766+
}
767+
768+
// packageInstallErrorHandler checks for two kinds of errors to print custom messages for so that Devbox users
769+
// can work around them:
770+
// 1. Packages that cannot be installed on the current system, but may be installable on other systems.packageInstallErrorHandler
771+
// 2. Packages marked insecure by nix
772+
func packageInstallErrorHandler(err error, pkg *Package, installableOrEmpty string) error {
773+
if err == nil {
774+
return nil
775+
}
776+
777+
// Check if the user is installing a package that cannot be installed on their platform.
778+
// For example, glibcLocales on MacOS will give the following error:
779+
// flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path
780+
// This is because glibcLocales is only available on Linux.
781+
// The user should try `devbox add` again with `--exclude-platform`
782+
errMessage := strings.TrimSpace(err.Error())
783+
784+
// Sample error from `devbox add glibcLocales` on a mac:
785+
// error: flake output attribute 'legacyPackages.x86_64-darwin.glibcLocales' is not a derivation or path
786+
maybePackageSystemCompatibilityErrorType1 := strings.Contains(errMessage, "error: flake output attribute") &&
787+
strings.Contains(errMessage, "is not a derivation or path")
788+
// Sample error from `devbox add sublime4` on a mac:
789+
// error: Package ‘sublimetext4-4169’ in /nix/store/nlbjx0mp83p2qzf1rkmzbgvq1wxfir81-source/pkgs/applications/editors/sublime/4/common.nix:168 is not available on the requested hostPlatform:
790+
// hostPlatform.config = "x86_64-apple-darwin"
791+
// package.meta.platforms = [
792+
// "aarch64-linux"
793+
// "x86_64-linux"
794+
// ]
795+
maybePackageSystemCompatibilityErrorType2 := strings.Contains(errMessage, "is not available on the requested hostPlatform")
796+
797+
if maybePackageSystemCompatibilityErrorType1 || maybePackageSystemCompatibilityErrorType2 {
798+
platform := nix.System()
799+
return usererr.WithUserMessage(
800+
err,
801+
"package %s cannot be installed on your platform %s.\n"+
802+
"If you know this package is incompatible with %[2]s, then "+
803+
"you could run `devbox add %[1]s --exclude-platform %[2]s` and re-try.\n"+
804+
"If you think this package should be compatible with %[2]s, then "+
805+
"it's possible this particular version is not available yet from the nix registry. "+
806+
"You could try `devbox add` with a different version for this package.\n\n"+
807+
"Underlying Error from nix is:",
808+
pkg.Versioned(),
809+
platform,
810+
)
811+
}
812+
813+
if isInsecureErr, userErr := nix.IsExitErrorInsecurePackage(err, pkg.Versioned(), installableOrEmpty); isInsecureErr {
814+
return userErr
815+
}
816+
817+
return usererr.WithUserMessage(err, "error installing package %s", pkg.Raw)
818+
}

0 commit comments

Comments
 (0)