Skip to content

Commit 2350684

Browse files
authored
[versioned] Deal with packages that don't build on system (#996)
## Summary Fixes a few bugs: * Remove panic * If package can't be found for current system, search on all systems so we can return better error message. * Don't automatically write to disk when resolving lockfile packages. That way if there's an error we don't leave the lockfile in a bad state. ## How was it tested? ### package that exists but not for my system ```bash ➜ devbox git:(landau/fix-panic) ✗ devbox add ripgrep@12 There was an error installing nix packages: ripgrep@12. Packages were not added to devbox.json Error: Package "ripgrep@12" was found, but we're unable to build it for your system. You may need to choose another version or write a custom flake. ``` ### package that doesn't exist ```bash ➜ devbox git:(landau/fix-panic) ✗ devbox add foobar1@8 Error: foobar1@8: package not found To search for packages use https://search.nixos.org/packages ``` ### regular versioned install: ```bash devbox add php@8 ``` ### Regular install: ```bash devbox add php ``` ### Manual add * Edited devbox.json and added `php@8` and confirmed lockfile was updated after `devbox run`
1 parent 5945705 commit 2350684

File tree

7 files changed

+145
-84
lines changed

7 files changed

+145
-84
lines changed

internal/impl/devbox.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,11 @@ func (d *Devbox) NixPkgsCommitHash() string {
152152

153153
func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
154154
shellPlan := planner.GetShellPlan(d.projectDir, d.packages())
155-
shellPlan.FlakeInputs = d.flakeInputs()
155+
var err error
156+
shellPlan.FlakeInputs, err = d.flakeInputs()
157+
if err != nil {
158+
return nil, err
159+
}
156160

157161
nixpkgsInfo := plansdk.GetNixpkgsInfo(d.cfg.Nixpkgs.Commit)
158162

internal/impl/flakes.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import (
1414
// created by devbox. We map packages to the correct flake and attribute path
1515
// and group flakes by URL to avoid duplication. All inputs should be locked
1616
// i.e. have a commit hash and always resolve to the same package/version.
17-
func (d *Devbox) flakeInputs() []*plansdk.FlakeInput {
17+
func (d *Devbox) flakeInputs() ([]*plansdk.FlakeInput, error) {
1818
inputs := map[string]*plansdk.FlakeInput{}
1919
for _, p := range d.packages() {
2020
pkg := nix.InputFromString(p, d.lockfile)
2121
AttributePath, err := pkg.PackageAttributePath()
2222
if err != nil {
23-
panic(err)
23+
return nil, err
2424
}
2525
if input, ok := inputs[pkg.URLForInput()]; !ok {
2626
inputs[pkg.URLForInput()] = &plansdk.FlakeInput{
@@ -35,5 +35,5 @@ func (d *Devbox) flakeInputs() []*plansdk.FlakeInput {
3535
}
3636
}
3737

38-
return lo.Values(inputs)
38+
return lo.Values(inputs), nil
3939
}

internal/impl/packages.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,12 @@ const (
150150
func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMode) error {
151151
defer trace.StartRegion(ctx, "ensurePackages").End()
152152

153-
lock, err := lock.Local(d)
153+
localLock, err := lock.Local(d)
154154
if err != nil {
155155
return err
156156
}
157157

158-
upToDate, err := lock.IsUpToDate()
158+
upToDate, err := localLock.IsUpToDate()
159159
if err != nil {
160160
return err
161161
}
@@ -183,7 +183,12 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
183183
return err
184184
}
185185

186-
return lock.Update()
186+
if err = localLock.Update(); err != nil {
187+
return err
188+
}
189+
190+
// Update lockfile to ensure any newly resolved packages are saved to disk.
191+
return d.lockfile.Save()
187192
}
188193

189194
func (d *Devbox) profilePath() (string, error) {

internal/lock/lockfile.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,18 @@ func (l *File) Add(pkgs ...string) error {
5757
}
5858
}
5959
}
60-
return nil
60+
return l.Save()
6161
}
6262

6363
func (l *File) Remove(pkgs ...string) error {
6464
for _, p := range pkgs {
6565
delete(l.Packages, p)
6666
}
67-
return l.Update()
67+
return l.Save()
6868
}
6969

70+
// Resolve updates the in memory copy for performance but does not write to disk
71+
// This avoids writing values that may need to be removed in case of error.
7072
func (l *File) Resolve(pkg string) (*Package, error) {
7173
if entry, ok := l.Packages[pkg]; !ok || entry.Resolved == "" {
7274
var locked *Package
@@ -82,9 +84,6 @@ func (l *File) Resolve(pkg string) (*Package, error) {
8284
locked = &Package{Resolved: l.LegacyNixpkgsPath(pkg)}
8385
}
8486
l.Packages[pkg] = locked
85-
if err := l.Update(); err != nil {
86-
return nil, err
87-
}
8887
}
8988

9089
return l.Packages[pkg], nil
@@ -99,7 +98,7 @@ func (l *File) Entry(pkg string) *Package {
9998
return l.Packages[pkg]
10099
}
101100

102-
func (l *File) Update() error {
101+
func (l *File) Save() error {
103102
// Never write lockfile if versioned packages is not enabled
104103
if !featureflag.LockFile.Enabled() {
105104
return nil

internal/nix/input.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,19 +108,21 @@ func (i *Input) URLForInstall() (string, error) {
108108
// references is returns the full path to the package in the flake. e.g.
109109
// packages.x86_64-linux.hello
110110
func (i *Input) PackageAttributePath() (string, error) {
111-
var infos map[string]*Info
111+
var query string
112112
if i.isVersioned() {
113113
entry, err := i.lockfile.Resolve(i.String())
114114
if err != nil {
115115
return "", err
116116
}
117-
infos = search(entry.Resolved)
117+
query = entry.Resolved
118118
} else if i.IsDevboxPackage() {
119-
infos = search(i.lockfile.LegacyNixpkgsPath(i.String()))
119+
query = i.lockfile.LegacyNixpkgsPath(i.String())
120120
} else {
121-
infos = search(i.String())
121+
query = i.String()
122122
}
123123

124+
infos := search(query)
125+
124126
if len(infos) == 1 {
125127
return lo.Keys(infos)[0], nil
126128
}
@@ -153,6 +155,14 @@ func (i *Input) PackageAttributePath() (string, error) {
153155
)
154156
}
155157

158+
if pkgExistsForAnySystem(query) {
159+
return "", usererr.New(
160+
"Package \"%s\" was found, but we're unable to build it for your system."+
161+
" You may need to choose another version or write a custom flake.",
162+
i.String(),
163+
)
164+
}
165+
156166
return "", usererr.New("Package \"%s\" was not found", i.String())
157167
}
158168

internal/nix/nix.go

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,89 +6,22 @@ package nix
66
import (
77
"context"
88
"encoding/json"
9-
"fmt"
109
"io/fs"
1110
"os"
1211
"os/exec"
1312
"path/filepath"
1413
"runtime/trace"
1514

1615
"github.com/pkg/errors"
17-
"github.com/samber/lo"
1816

1917
"go.jetpack.io/devbox/internal/debug"
20-
"go.jetpack.io/devbox/internal/lock"
2118
)
2219

2320
// ProfilePath contains the contents of the profile generated via `nix-env --profile ProfilePath <command>`
2421
// or `nix profile install --profile ProfilePath <package...>`
2522
// Instead of using directory, prefer using the devbox.ProfileDir() function that ensures the directory exists.
2623
const ProfilePath = ".devbox/nix/profile/default"
2724

28-
var ErrPackageNotFound = errors.New("package not found")
29-
var ErrPackageNotInstalled = errors.New("package not installed")
30-
31-
func PkgExists(pkg string, lock *lock.File) (bool, error) {
32-
return InputFromString(pkg, lock).validateExists()
33-
}
34-
35-
type Info struct {
36-
// attribute key is different in flakes vs legacy so we should only use it
37-
// if we know exactly which version we are using
38-
attributeKey string
39-
PName string
40-
Version string
41-
}
42-
43-
func (i *Info) String() string {
44-
return fmt.Sprintf("%s-%s", i.PName, i.Version)
45-
}
46-
47-
func PkgInfo(nixpkgsCommit, pkg string) *Info {
48-
exactPackage := fmt.Sprintf("%s#%s", FlakeNixpkgs(nixpkgsCommit), pkg)
49-
if nixpkgsCommit == "" {
50-
exactPackage = fmt.Sprintf("nixpkgs#%s", pkg)
51-
}
52-
53-
results := search(exactPackage)
54-
if len(results) == 0 {
55-
return nil
56-
}
57-
// we should only have one result
58-
return lo.Values(results)[0]
59-
}
60-
61-
func search(url string) map[string]*Info {
62-
cmd := exec.Command("nix", "search", "--json", url)
63-
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
64-
cmd.Stderr = os.Stderr
65-
debug.Log("running command: %s\n", cmd)
66-
out, err := cmd.Output()
67-
if err != nil {
68-
// for now, assume all errors are invalid packages.
69-
return nil
70-
}
71-
return parseSearchResults(out)
72-
}
73-
74-
func parseSearchResults(data []byte) map[string]*Info {
75-
var results map[string]map[string]any
76-
err := json.Unmarshal(data, &results)
77-
if err != nil {
78-
panic(err)
79-
}
80-
infos := map[string]*Info{}
81-
for key, result := range results {
82-
infos[key] = &Info{
83-
attributeKey: key,
84-
PName: result["pname"].(string),
85-
Version: result["version"].(string),
86-
}
87-
88-
}
89-
return infos
90-
}
91-
9225
type PrintDevEnvOut struct {
9326
Variables map[string]Variable // the key is the name.
9427
}

internal/nix/search.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package nix
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
9+
"github.com/pkg/errors"
10+
"github.com/samber/lo"
11+
"go.jetpack.io/devbox/internal/debug"
12+
"go.jetpack.io/devbox/internal/lock"
13+
)
14+
15+
var ErrPackageNotFound = errors.New("package not found")
16+
var ErrPackageNotInstalled = errors.New("package not installed")
17+
18+
func PkgExists(pkg string, lock *lock.File) (bool, error) {
19+
return InputFromString(pkg, lock).validateExists()
20+
}
21+
22+
type Info struct {
23+
// attribute key is different in flakes vs legacy so we should only use it
24+
// if we know exactly which version we are using
25+
attributeKey string
26+
PName string
27+
Version string
28+
}
29+
30+
func (i *Info) String() string {
31+
return fmt.Sprintf("%s-%s", i.PName, i.Version)
32+
}
33+
34+
func PkgInfo(nixpkgsCommit, pkg string) *Info {
35+
exactPackage := fmt.Sprintf("%s#%s", FlakeNixpkgs(nixpkgsCommit), pkg)
36+
if nixpkgsCommit == "" {
37+
exactPackage = fmt.Sprintf("nixpkgs#%s", pkg)
38+
}
39+
40+
results := search(exactPackage)
41+
if len(results) == 0 {
42+
return nil
43+
}
44+
// we should only have one result
45+
return lo.Values(results)[0]
46+
}
47+
48+
func search(url string) map[string]*Info {
49+
return searchSystem(url, "")
50+
}
51+
52+
func parseSearchResults(data []byte) map[string]*Info {
53+
var results map[string]map[string]any
54+
err := json.Unmarshal(data, &results)
55+
if err != nil {
56+
panic(err)
57+
}
58+
infos := map[string]*Info{}
59+
for key, result := range results {
60+
infos[key] = &Info{
61+
attributeKey: key,
62+
PName: result["pname"].(string),
63+
Version: result["version"].(string),
64+
}
65+
66+
}
67+
return infos
68+
}
69+
70+
// pkgExistsForAnySystem is a bit slow (~600ms). Only use it if there's already
71+
// been an error and we want to provide a better error message.
72+
func pkgExistsForAnySystem(pkg string) bool {
73+
systems := []string{
74+
// Check most common systems first.
75+
"x86_64-linux",
76+
"x86_64-darwin",
77+
"aarch64-linux",
78+
"aarch64-darwin",
79+
80+
"armv5tel-linux",
81+
"armv6l-linux",
82+
"armv7l-linux",
83+
"i686-linux",
84+
"mipsel-linux",
85+
"powerpc64le-linux",
86+
"riscv64-linux",
87+
}
88+
for _, system := range systems {
89+
if len(searchSystem(pkg, system)) > 0 {
90+
return true
91+
}
92+
}
93+
return false
94+
}
95+
96+
func searchSystem(url string, system string) map[string]*Info {
97+
cmd := exec.Command("nix", "search", "--json", url)
98+
cmd.Args = append(cmd.Args, ExperimentalFlags()...)
99+
if system != "" {
100+
cmd.Args = append(cmd.Args, "--system", system)
101+
}
102+
cmd.Stderr = os.Stderr
103+
debug.Log("running command: %s\n", cmd)
104+
out, err := cmd.Output()
105+
if err != nil {
106+
// for now, assume all errors are invalid packages.
107+
return nil
108+
}
109+
return parseSearchResults(out)
110+
}

0 commit comments

Comments
 (0)