diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index 6c8d7319e75..9c7dd9a7c15 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -133,6 +133,15 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt // about not building on the current system, since user's can continue // via --exclude-platform flag. packageNameForConfig = pkg.Versioned() + } else if errors.Is(err, nix.ErrPackageNotFound) && + versionedPkg.IsDevboxPackage && + !strings.Contains(pkg.CanonicalName(), ".") { + // Heuristic: search indexes top-level nixpkgs packages, so falling + // back to nixpkgs (which requires a slow download) won't find it + // either. Nested attribute paths (e.g. stdenv.cc.cc.lib, openssl.dev, + // python3Packages.requests) are not all indexed, so those still fall + // back below. + return packageNotFoundError(pkg) } else if !versionedPkg.IsDevboxPackage { // This means it didn't validate and we don't want to fallback to legacy // Just propagate the error. @@ -146,7 +155,7 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt if err != nil { // This means it looked like a devbox package or attribute path, but we // could not find it in search or in the legacy nixpkgs path. - return usererr.New("Package %s not found", pkg.Raw) + return packageNotFoundError(pkg) } } @@ -171,6 +180,13 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt return d.printPostAddMessage(ctx, pkgs, unchangedPackageNames, opts) } +func packageNotFoundError(pkg *devpkg.Package) error { + return usererr.New( + "Package %q not found. To search for packages, use `devbox search %s`", + pkg.Raw, pkg.CanonicalName(), + ) +} + func (d *Devbox) setPackageOptions(pkgs []string, opts devopt.AddOpts) error { for _, pkg := range pkgs { if err := d.cfg.PackageMutator().AddPlatforms( diff --git a/internal/lock/resolve.go b/internal/lock/resolve.go index 2183d7c6024..402b6f16618 100644 --- a/internal/lock/resolve.go +++ b/internal/lock/resolve.go @@ -68,9 +68,12 @@ func (f *File) FetchResolvedPackage(pkg string, refresh bool) (*Package, error) } packageVersion, err := searcher.Client().Resolve(name, version) - if err != nil { + if errors.Is(err, searcher.ErrNotFound) { return nil, errors.Wrapf(nix.ErrPackageNotFound, "%s@%s", name, version) } + if err != nil { + return nil, err + } sysInfos, err := buildLockSystemInfos(packageVersion) if err != nil { diff --git a/internal/lock/resolve_test.go b/internal/lock/resolve_test.go new file mode 100644 index 00000000000..12b3efe6117 --- /dev/null +++ b/internal/lock/resolve_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 Jetify Inc. and contributors. All rights reserved. +// Use of this source code is governed by the license in the LICENSE file. + +package lock + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "go.jetify.com/devbox/internal/envir" + "go.jetify.com/devbox/internal/nix" +) + +func TestFetchResolvedPackageErrors(t *testing.T) { + for _, resolveV2 := range []string{"0", "1"} { + for _, test := range []struct { + status int + wantNotFound bool + }{ + {http.StatusNotFound, true}, + {http.StatusInternalServerError, false}, + } { + t.Run("v2="+resolveV2+"/"+http.StatusText(test.status), func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(test.status) + })) + t.Cleanup(srv.Close) + t.Setenv(envir.DevboxSearchHost, srv.URL) + t.Setenv("DEVBOX_FEATURE_RESOLVE_V2", resolveV2) + + _, err := (&File{}).FetchResolvedPackage("hello@latest", false) + if err == nil { + t.Fatal("got nil error, want an error") + } + if got := errors.Is(err, nix.ErrPackageNotFound); got != test.wantNotFound { + t.Errorf("errors.Is(err, nix.ErrPackageNotFound) = %v, want %v (err: %v)", got, test.wantNotFound, err) + } + }) + } + } +} diff --git a/testscripts/add/add_not_found.test.txt b/testscripts/add/add_not_found.test.txt new file mode 100644 index 00000000000..a690c52f2da --- /dev/null +++ b/testscripts/add/add_not_found.test.txt @@ -0,0 +1,20 @@ +# Testscript for adding a package that doesn't exist. + +exec devbox init + +# A bare name that search doesn't know should fail fast without +# downloading nixpkgs. +! exec devbox add inotifywait +stderr 'Package "inotifywait" not found. To search for packages, use `devbox search inotifywait`' +! stderr 'Ensuring nixpkgs registry is downloaded' + +# The search hint drops the version. +! exec devbox add hello@99999 +stderr 'Package "hello@99999" not found. To search for packages, use `devbox search hello`' + +json.superset devbox.json expected_devbox.json + +-- expected_devbox.json -- +{ + "packages": [] +}