Skip to content

[errors] Show nicer error when installing a package that is not supported by current system #1279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 15 additions & 32 deletions internal/devpkg/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ func (p *Package) isLocal() bool {
return p.Scheme == "path"
}

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

Expand Down Expand Up @@ -135,7 +135,7 @@ func (p *Package) FlakeInputName() string {
// URLForFlakeInput returns the input url to be used in a flake.nix file. This
// input can be used to import the package.
func (p *Package) URLForFlakeInput() string {
if p.isDevboxPackage() {
if p.IsDevboxPackage() {
entry, err := p.lockfile.Resolve(p.Raw)
if err != nil {
panic(err)
Expand Down Expand Up @@ -175,7 +175,7 @@ func (p *Package) Installable() (string, error) {
// The key difference with URLForFlakeInput is that it has a suffix of
// `#attributePath`
func (p *Package) urlForInstall() (string, error) {
if p.isDevboxPackage() {
if p.IsDevboxPackage() {
entry, err := p.lockfile.Resolve(p.Raw)
if err != nil {
return "", err
Expand All @@ -190,7 +190,7 @@ func (p *Package) urlForInstall() (string, error) {
}

func (p *Package) NormalizedDevboxPackageReference() (string, error) {
if !p.isDevboxPackage() {
if !p.IsDevboxPackage() {
return "", nil
}

Expand All @@ -201,7 +201,7 @@ func (p *Package) NormalizedDevboxPackageReference() (string, error) {
return "", err
}
path = entry.Resolved
} else if p.isDevboxPackage() {
} else if p.IsDevboxPackage() {
path = p.lockfile.LegacyNixpkgsPath(p.String())
}

Expand All @@ -220,7 +220,7 @@ func (p *Package) NormalizedDevboxPackageReference() (string, error) {
// PackageAttributePath returns the short attribute path for a package which
// does not include packages/legacyPackages or the system name.
func (p *Package) PackageAttributePath() (string, error) {
if p.isDevboxPackage() {
if p.IsDevboxPackage() {
entry, err := p.lockfile.Resolve(p.Raw)
if err != nil {
return "", err
Expand All @@ -236,7 +236,7 @@ func (p *Package) PackageAttributePath() (string, error) {
// During happy paths (devbox packages and nix flakes that contains a fragment)
// it is much faster than NormalizedPackageAttributePath
func (p *Package) FullPackageAttributePath() (string, error) {
if p.isDevboxPackage() {
if p.IsDevboxPackage() {
reference, err := p.NormalizedDevboxPackageReference()
if err != nil {
return "", err
Expand Down Expand Up @@ -266,7 +266,7 @@ func (p *Package) NormalizedPackageAttributePath() (string, error) {
// path. It is an expensive call (~100ms).
func (p *Package) normalizePackageAttributePath() (string, error) {
var query string
if p.isDevboxPackage() {
if p.IsDevboxPackage() {
if p.isVersioned() {
entry, err := p.lockfile.Resolve(p.Raw)
if err != nil {
Expand All @@ -282,7 +282,7 @@ func (p *Package) normalizePackageAttributePath() (string, error) {

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

if len(infos) == 1 {
return lo.Keys(infos)[0], nil
Expand Down Expand Up @@ -349,23 +349,6 @@ func (p *Package) Hash() string {
return shortHash
}

func (p *Package) ValidateExists() (bool, error) {
if p.isVersioned() && p.version() == "" {
return false, usererr.New("No version specified for %q.", p.Path)
}

inCache, err := p.IsInBinaryCache()
if err != nil {
return false, err
}
if inCache {
return true, nil
}

info, err := p.NormalizedPackageAttributePath()
return info != "", err
}

func (p *Package) Equals(other *Package) bool {
if p.String() == other.String() {
return true
Expand All @@ -390,22 +373,22 @@ func (p *Package) Equals(other *Package) bool {
// CanonicalName returns the name of the package without the version
// it only applies to devbox packages
func (p *Package) CanonicalName() string {
if !p.isDevboxPackage() {
if !p.IsDevboxPackage() {
return ""
}
name, _, _ := strings.Cut(p.Path, "@")
return name
}

func (p *Package) Versioned() string {
if p.isDevboxPackage() && !p.isVersioned() {
if p.IsDevboxPackage() && !p.isVersioned() {
return p.Raw + "@latest"
}
return p.Raw
}

func (p *Package) IsLegacy() bool {
return p.isDevboxPackage() && !p.isVersioned() && p.lockfile.Get(p.Raw).GetSource() == ""
return p.IsDevboxPackage() && !p.isVersioned() && p.lockfile.Get(p.Raw).GetSource() == ""
}

func (p *Package) LegacyToVersioned() string {
Expand Down Expand Up @@ -437,15 +420,15 @@ func (p *Package) EnsureNixpkgsPrefetched(w io.Writer) error {
// version returns the version of the package
// it only applies to devbox packages
func (p *Package) version() string {
if !p.isDevboxPackage() {
if !p.IsDevboxPackage() {
return ""
}
_, version, _ := strings.Cut(p.Path, "@")
return version
}

func (p *Package) isVersioned() bool {
return p.isDevboxPackage() && strings.Contains(p.Path, "@")
return p.IsDevboxPackage() && strings.Contains(p.Path, "@")
}

func (p *Package) HashFromNixPkgsURL() string {
Expand Down
43 changes: 43 additions & 0 deletions internal/devpkg/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package devpkg

import (
"strings"

"go.jetpack.io/devbox/internal/boxcli/usererr"
"go.jetpack.io/devbox/internal/nix"
)

func (p *Package) ValidateExists() (bool, error) {
if p.isVersioned() && p.version() == "" {
return false, usererr.New("No version specified for %q.", p.Path)
}

inCache, err := p.IsInBinaryCache()
if err != nil {
return false, err
}
if inCache {
return true, nil
}

info, err := p.NormalizedPackageAttributePath()
return info != "", err
}

func (p *Package) ValidateInstallsOnSystem() (bool, error) {
u, err := p.urlForInstall()
if err != nil {
return false, err
}
info, _ := nix.Search(u)
if len(info) == 0 {
return false, nil
}
if out, err := nix.Eval(u); err != nil &&
strings.Contains(string(out), "is not available on the requested hostPlatform") {
return false, nil
}
// There's other stuff that may cause this evaluation to fail, but we don't
// want to handle all of them here. (e.g. unfree packages)
return true, nil
}
2 changes: 1 addition & 1 deletion internal/impl/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func (d *Devbox) Info(ctx context.Context, pkg string, markdown bool) error {
return err
}

results := nix.Search(locked.Resolved)
results, _ := nix.Search(locked.Resolved)
if len(results) == 0 {
_, err := fmt.Fprintf(d.writer, "Package %s not found\n", pkg)
return errors.WithStack(err)
Expand Down
23 changes: 14 additions & 9 deletions internal/impl/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,16 +66,20 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames ...string) error {
// validate that the versioned package exists in the search endpoint.
// if not, fallback to legacy vanilla nix.
versionedPkg := devpkg.PackageFromString(pkg.Versioned(), d.lockfile)
ok, err := versionedPkg.ValidateExists()

packageNameForConfig := pkg.Raw
if err == nil && ok {
if ok, err := versionedPkg.ValidateExists(); err == nil && ok {
// Only use versioned if it exists in search.
packageNameForConfig = pkg.Versioned()
} else if !versionedPkg.IsDevboxPackage() {
// This means it didn't validate and we don't want to fallback to legacy
// Just propagate the error.
return err
} else if _, err := nix.Search(d.lockfile.LegacyNixpkgsPath(pkg.Raw)); 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)
}
// else {
// // TODO (landau): use nix.Search to check if this package exists
// // fallthrough and treat package as a legacy package.
// }

d.cfg.Packages = append(d.cfg.Packages, packageNameForConfig)
addedPackageNames = append(addedPackageNames, packageNameForConfig)
Expand Down Expand Up @@ -197,9 +201,6 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
return nil
}

if err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {
return err
}
if mode == ensure {
fmt.Fprintln(d.writer, "Ensuring packages are installed.")
}
Expand All @@ -208,6 +209,10 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
return err
}

if err := shellgen.GenerateForPrintEnv(ctx, d); err != nil {
return err
}

if err := plugin.RemoveInvalidSymlinks(d.projectDir); err != nil {
return err
}
Expand Down
8 changes: 8 additions & 0 deletions internal/nix/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ func PackageKnownVulnerabilities(path string) []string {
return vulnerabilities
}

// Eval is raw nix eval. Needs to be parsed. Useful for stuff like
// nix eval --raw nixpkgs/9ef09e06806e79e32e30d17aee6879d69c011037#fuse3
// to determine if a package if a package can be installed in system.
func Eval(path string) ([]byte, error) {
cmd := command("eval", "--raw", path)
return cmd.CombinedOutput()
}

func AllowInsecurePackages() {
os.Setenv("NIXPKGS_ALLOW_INSECURE", "1")
}
Expand Down
4 changes: 2 additions & 2 deletions internal/nix/nixpkgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ func nixpkgsCommitFilePath() string {
// github:NixOS/nixpkgs/<hash> as the URL. If the user wishes to reference nixpkgs
// themselves, this function may not return true.
func IsGithubNixpkgsURL(url string) bool {
return strings.HasPrefix(url, "github:NixOS/nixpkgs/")
return strings.HasPrefix(strings.ToLower(url), "github:nixos/nixpkgs/")
}

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

// HashFromNixPkgsURL will (for example) return 5233fd2ba76a3accb5aaa999c00509a11fd0793c
// from github:nixos/nixpkgs/5233fd2ba76a3accb5aaa999c00509a11fd0793c#hello
Expand Down
9 changes: 9 additions & 0 deletions internal/nix/nixprofile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/fatih/color"
"github.com/pkg/errors"
"go.jetpack.io/devbox/internal/boxcli/usererr"
"go.jetpack.io/devbox/internal/devpkg"
"go.jetpack.io/devbox/internal/nix"

Expand Down Expand Up @@ -219,6 +220,14 @@ func ProfileInstall(args *ProfileInstallArgs) error {
if err := nix.EnsureNixpkgsPrefetched(args.Writer, input.HashFromNixPkgsURL()); err != nil {
return err
}
if exists, err := input.ValidateInstallsOnSystem(); err != nil {
return err
} else if !exists {
return usererr.New(
"package %s cannot be installed on your system. It may be installable on other systems.",
input.String(),
)
}
}
stepMsg := args.Package
if args.CustomStepMessage != "" {
Expand Down
13 changes: 7 additions & 6 deletions internal/nix/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"

"github.com/pkg/errors"
"go.jetpack.io/devbox/internal/boxcli/usererr"
"go.jetpack.io/devbox/internal/debug"
)

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

func Search(url string) map[string]*Info {
func Search(url string) (map[string]*Info, error) {
return searchSystem(url, "")
}

Expand Down Expand Up @@ -66,14 +67,15 @@ func PkgExistsForAnySystem(pkg string) bool {
"riscv64-linux",
}
for _, system := range systems {
if len(searchSystem(pkg, system)) > 0 {
results, _ := searchSystem(pkg, system)
if len(results) > 0 {
return true
}
}
return false
}

func searchSystem(url string, system string) map[string]*Info {
func searchSystem(url string, system string) (map[string]*Info, error) {
// Eventually we may pass a writer here, but for now it is safe to use stderr
writer := os.Stderr
// Search will download nixpkgs if it's not already downloaded. Adding this
Expand All @@ -90,12 +92,11 @@ func searchSystem(url string, system string) map[string]*Info {
if system != "" {
cmd.Args = append(cmd.Args, "--system", system)
}
cmd.Stderr = writer
debug.Log("running command: %s\n", cmd)
out, err := cmd.Output()
if err != nil {
// for now, assume all errors are invalid packages.
return nil
return nil, usererr.NewExecError(err)
}
return parseSearchResults(out)
return parseSearchResults(out), nil
}