Skip to content

Commit 778c8d2

Browse files
authored
[errors] Show nicer error when installing a package that is not supported by current system (#1279)
## Summary Shows better error messages in following cases: * When package doesn't exist on search or in nixpkgs as attribute path * When package is not supported by current platform ## How was it tested? `devbox add glibcLocales@2.37-8` `devbox add fuse3` <img width="738" alt="image" src="https://github.com/jetpack-io/devbox/assets/544948/5440e5f5-ae4d-4769-aeea-d4493df1bbbf">
1 parent c744b7b commit 778c8d2

File tree

8 files changed

+99
-50
lines changed

8 files changed

+99
-50
lines changed

internal/devpkg/package.go

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ func (p *Package) isLocal() bool {
9191
return p.Scheme == "path"
9292
}
9393

94-
// isDevboxPackage specifies whether this package is a devbox package. Devbox
94+
// IsDevboxPackage specifies whether this package is a devbox package. Devbox
9595
// packages have the format `canonicalName@version`and can be resolved by devbox
9696
// search. This also returns true for legacy packages which are just an
9797
// attribute path. An explicit flake reference is _not_ a devbox package.
98-
func (p *Package) isDevboxPackage() bool {
98+
func (p *Package) IsDevboxPackage() bool {
9999
return p.Scheme == ""
100100
}
101101

@@ -135,7 +135,7 @@ func (p *Package) FlakeInputName() string {
135135
// URLForFlakeInput returns the input url to be used in a flake.nix file. This
136136
// input can be used to import the package.
137137
func (p *Package) URLForFlakeInput() string {
138-
if p.isDevboxPackage() {
138+
if p.IsDevboxPackage() {
139139
entry, err := p.lockfile.Resolve(p.Raw)
140140
if err != nil {
141141
panic(err)
@@ -175,7 +175,7 @@ func (p *Package) Installable() (string, error) {
175175
// The key difference with URLForFlakeInput is that it has a suffix of
176176
// `#attributePath`
177177
func (p *Package) urlForInstall() (string, error) {
178-
if p.isDevboxPackage() {
178+
if p.IsDevboxPackage() {
179179
entry, err := p.lockfile.Resolve(p.Raw)
180180
if err != nil {
181181
return "", err
@@ -190,7 +190,7 @@ func (p *Package) urlForInstall() (string, error) {
190190
}
191191

192192
func (p *Package) NormalizedDevboxPackageReference() (string, error) {
193-
if !p.isDevboxPackage() {
193+
if !p.IsDevboxPackage() {
194194
return "", nil
195195
}
196196

@@ -201,7 +201,7 @@ func (p *Package) NormalizedDevboxPackageReference() (string, error) {
201201
return "", err
202202
}
203203
path = entry.Resolved
204-
} else if p.isDevboxPackage() {
204+
} else if p.IsDevboxPackage() {
205205
path = p.lockfile.LegacyNixpkgsPath(p.String())
206206
}
207207

@@ -220,7 +220,7 @@ func (p *Package) NormalizedDevboxPackageReference() (string, error) {
220220
// PackageAttributePath returns the short attribute path for a package which
221221
// does not include packages/legacyPackages or the system name.
222222
func (p *Package) PackageAttributePath() (string, error) {
223-
if p.isDevboxPackage() {
223+
if p.IsDevboxPackage() {
224224
entry, err := p.lockfile.Resolve(p.Raw)
225225
if err != nil {
226226
return "", err
@@ -236,7 +236,7 @@ func (p *Package) PackageAttributePath() (string, error) {
236236
// During happy paths (devbox packages and nix flakes that contains a fragment)
237237
// it is much faster than NormalizedPackageAttributePath
238238
func (p *Package) FullPackageAttributePath() (string, error) {
239-
if p.isDevboxPackage() {
239+
if p.IsDevboxPackage() {
240240
reference, err := p.NormalizedDevboxPackageReference()
241241
if err != nil {
242242
return "", err
@@ -266,7 +266,7 @@ func (p *Package) NormalizedPackageAttributePath() (string, error) {
266266
// path. It is an expensive call (~100ms).
267267
func (p *Package) normalizePackageAttributePath() (string, error) {
268268
var query string
269-
if p.isDevboxPackage() {
269+
if p.IsDevboxPackage() {
270270
if p.isVersioned() {
271271
entry, err := p.lockfile.Resolve(p.Raw)
272272
if err != nil {
@@ -282,7 +282,7 @@ func (p *Package) normalizePackageAttributePath() (string, error) {
282282

283283
// We prefer search over just trying to parse the URL because search will
284284
// guarantee that the package exists for the current system.
285-
infos := nix.Search(query)
285+
infos, _ := nix.Search(query)
286286

287287
if len(infos) == 1 {
288288
return lo.Keys(infos)[0], nil
@@ -349,23 +349,6 @@ func (p *Package) Hash() string {
349349
return shortHash
350350
}
351351

352-
func (p *Package) ValidateExists() (bool, error) {
353-
if p.isVersioned() && p.version() == "" {
354-
return false, usererr.New("No version specified for %q.", p.Path)
355-
}
356-
357-
inCache, err := p.IsInBinaryCache()
358-
if err != nil {
359-
return false, err
360-
}
361-
if inCache {
362-
return true, nil
363-
}
364-
365-
info, err := p.NormalizedPackageAttributePath()
366-
return info != "", err
367-
}
368-
369352
func (p *Package) Equals(other *Package) bool {
370353
if p.String() == other.String() {
371354
return true
@@ -390,22 +373,22 @@ func (p *Package) Equals(other *Package) bool {
390373
// CanonicalName returns the name of the package without the version
391374
// it only applies to devbox packages
392375
func (p *Package) CanonicalName() string {
393-
if !p.isDevboxPackage() {
376+
if !p.IsDevboxPackage() {
394377
return ""
395378
}
396379
name, _, _ := strings.Cut(p.Path, "@")
397380
return name
398381
}
399382

400383
func (p *Package) Versioned() string {
401-
if p.isDevboxPackage() && !p.isVersioned() {
384+
if p.IsDevboxPackage() && !p.isVersioned() {
402385
return p.Raw + "@latest"
403386
}
404387
return p.Raw
405388
}
406389

407390
func (p *Package) IsLegacy() bool {
408-
return p.isDevboxPackage() && !p.isVersioned() && p.lockfile.Get(p.Raw).GetSource() == ""
391+
return p.IsDevboxPackage() && !p.isVersioned() && p.lockfile.Get(p.Raw).GetSource() == ""
409392
}
410393

411394
func (p *Package) LegacyToVersioned() string {
@@ -437,15 +420,15 @@ func (p *Package) EnsureNixpkgsPrefetched(w io.Writer) error {
437420
// version returns the version of the package
438421
// it only applies to devbox packages
439422
func (p *Package) version() string {
440-
if !p.isDevboxPackage() {
423+
if !p.IsDevboxPackage() {
441424
return ""
442425
}
443426
_, version, _ := strings.Cut(p.Path, "@")
444427
return version
445428
}
446429

447430
func (p *Package) isVersioned() bool {
448-
return p.isDevboxPackage() && strings.Contains(p.Path, "@")
431+
return p.IsDevboxPackage() && strings.Contains(p.Path, "@")
449432
}
450433

451434
func (p *Package) HashFromNixPkgsURL() string {

internal/devpkg/validation.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package devpkg
2+
3+
import (
4+
"strings"
5+
6+
"go.jetpack.io/devbox/internal/boxcli/usererr"
7+
"go.jetpack.io/devbox/internal/nix"
8+
)
9+
10+
func (p *Package) ValidateExists() (bool, error) {
11+
if p.isVersioned() && p.version() == "" {
12+
return false, usererr.New("No version specified for %q.", p.Path)
13+
}
14+
15+
inCache, err := p.IsInBinaryCache()
16+
if err != nil {
17+
return false, err
18+
}
19+
if inCache {
20+
return true, nil
21+
}
22+
23+
info, err := p.NormalizedPackageAttributePath()
24+
return info != "", err
25+
}
26+
27+
func (p *Package) ValidateInstallsOnSystem() (bool, error) {
28+
u, err := p.urlForInstall()
29+
if err != nil {
30+
return false, err
31+
}
32+
info, _ := nix.Search(u)
33+
if len(info) == 0 {
34+
return false, nil
35+
}
36+
if out, err := nix.Eval(u); err != nil &&
37+
strings.Contains(string(out), "is not available on the requested hostPlatform") {
38+
return false, nil
39+
}
40+
// There's other stuff that may cause this evaluation to fail, but we don't
41+
// want to handle all of them here. (e.g. unfree packages)
42+
return true, nil
43+
}

internal/impl/devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ func (d *Devbox) Info(ctx context.Context, pkg string, markdown bool) error {
339339
return err
340340
}
341341

342-
results := nix.Search(locked.Resolved)
342+
results, _ := nix.Search(locked.Resolved)
343343
if len(results) == 0 {
344344
_, err := fmt.Fprintf(d.writer, "Package %s not found\n", pkg)
345345
return errors.WithStack(err)

internal/impl/packages.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,20 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
6666
// validate that the versioned package exists in the search endpoint.
6767
// if not, fallback to legacy vanilla nix.
6868
versionedPkg := devpkg.PackageFromString(pkg.Versioned(), d.lockfile)
69-
ok, err := versionedPkg.ValidateExists()
69+
7070
packageNameForConfig := pkg.Raw
71-
if err == nil && ok {
71+
if ok, err := versionedPkg.ValidateExists(); err == nil && ok {
7272
// Only use versioned if it exists in search.
7373
packageNameForConfig = pkg.Versioned()
74+
} else if !versionedPkg.IsDevboxPackage() {
75+
// This means it didn't validate and we don't want to fallback to legacy
76+
// Just propagate the error.
77+
return err
78+
} else if _, err := nix.Search(d.lockfile.LegacyNixpkgsPath(pkg.Raw)); err != nil {
79+
// This means it looked like a devbox package or attribute path, but we
80+
// could not find it in search or in the legacy nixpkgs path.
81+
return usererr.New("Package %s not found", pkg.Raw)
7482
}
75-
// else {
76-
// // TODO (landau): use nix.Search to check if this package exists
77-
// // fallthrough and treat package as a legacy package.
78-
// }
7983

8084
d.cfg.Packages = append(d.cfg.Packages, packageNameForConfig)
8185
addedPackageNames = append(addedPackageNames, packageNameForConfig)
@@ -197,9 +201,6 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
197201
return nil
198202
}
199203

200-
if err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {
201-
return err
202-
}
203204
if mode == ensure {
204205
fmt.Fprintln(d.writer, "Ensuring packages are installed.")
205206
}
@@ -208,6 +209,10 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
208209
return err
209210
}
210211

212+
if err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {
213+
return err
214+
}
215+
211216
if err := plugin.RemoveInvalidSymlinks(d.projectDir); err != nil {
212217
return err
213218
}

internal/nix/eval.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ func PackageKnownVulnerabilities(path string) []string {
4646
return vulnerabilities
4747
}
4848

49+
// Eval is raw nix eval. Needs to be parsed. Useful for stuff like
50+
// nix eval --raw nixpkgs/9ef09e06806e79e32e30d17aee6879d69c011037#fuse3
51+
// to determine if a package if a package can be installed in system.
52+
func Eval(path string) ([]byte, error) {
53+
cmd := command("eval", "--raw", path)
54+
return cmd.CombinedOutput()
55+
}
56+
4957
func AllowInsecurePackages() {
5058
os.Setenv("NIXPKGS_ALLOW_INSECURE", "1")
5159
}

internal/nix/nixpkgs.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ func nixpkgsCommitFilePath() string {
119119
// github:NixOS/nixpkgs/<hash> as the URL. If the user wishes to reference nixpkgs
120120
// themselves, this function may not return true.
121121
func IsGithubNixpkgsURL(url string) bool {
122-
return strings.HasPrefix(url, "github:NixOS/nixpkgs/")
122+
return strings.HasPrefix(strings.ToLower(url), "github:nixos/nixpkgs/")
123123
}
124124

125-
var hashFromNixPkgsRegex = regexp.MustCompile(`github:NixOS/nixpkgs/([^#]+).*`)
125+
var hashFromNixPkgsRegex = regexp.MustCompile(`(?i)github:nixos/nixpkgs/([^#]+).*`)
126126

127127
// HashFromNixPkgsURL will (for example) return 5233fd2ba76a3accb5aaa999c00509a11fd0793c
128128
// from github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello

internal/nix/nixprofile/profile.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/fatih/color"
1414
"github.com/pkg/errors"
15+
"go.jetpack.io/devbox/internal/boxcli/usererr"
1516
"go.jetpack.io/devbox/internal/devpkg"
1617
"go.jetpack.io/devbox/internal/nix"
1718

@@ -219,6 +220,14 @@ func ProfileInstall(args *ProfileInstallArgs) error {
219220
if err := nix.EnsureNixpkgsPrefetched(args.Writer, input.HashFromNixPkgsURL()); err != nil {
220221
return err
221222
}
223+
if exists, err := input.ValidateInstallsOnSystem(); err != nil {
224+
return err
225+
} else if !exists {
226+
return usererr.New(
227+
"package %s cannot be installed on your system. It may be installable on other systems.",
228+
input.String(),
229+
)
230+
}
222231
}
223232
stepMsg := args.Package
224233
if args.CustomStepMessage != "" {

internal/nix/search.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os/exec"
88

99
"github.com/pkg/errors"
10+
"go.jetpack.io/devbox/internal/boxcli/usererr"
1011
"go.jetpack.io/devbox/internal/debug"
1112
)
1213

@@ -25,7 +26,7 @@ func (i *Info) String() string {
2526
return fmt.Sprintf("%s-%s", i.PName, i.Version)
2627
}
2728

28-
func Search(url string) map[string]*Info {
29+
func Search(url string) (map[string]*Info, error) {
2930
return searchSystem(url, "")
3031
}
3132

@@ -66,14 +67,15 @@ func PkgExistsForAnySystem(pkg string) bool {
6667
"riscv64-linux",
6768
}
6869
for _, system := range systems {
69-
if len(searchSystem(pkg, system)) > 0 {
70+
results, _ := searchSystem(pkg, system)
71+
if len(results) > 0 {
7072
return true
7173
}
7274
}
7375
return false
7476
}
7577

76-
func searchSystem(url string, system string) map[string]*Info {
78+
func searchSystem(url string, system string) (map[string]*Info, error) {
7779
// Eventually we may pass a writer here, but for now it is safe to use stderr
7880
writer := os.Stderr
7981
// Search will download nixpkgs if it's not already downloaded. Adding this
@@ -90,12 +92,11 @@ func searchSystem(url string, system string) map[string]*Info {
9092
if system != "" {
9193
cmd.Args = append(cmd.Args, "--system", system)
9294
}
93-
cmd.Stderr = writer
9495
debug.Log("running command: %s\n", cmd)
9596
out, err := cmd.Output()
9697
if err != nil {
9798
// for now, assume all errors are invalid packages.
98-
return nil
99+
return nil, usererr.NewExecError(err)
99100
}
100-
return parseSearchResults(out)
101+
return parseSearchResults(out), nil
101102
}

0 commit comments

Comments
 (0)