Skip to content

Commit 391754f

Browse files
committed
fixes: experimental flags, UX improvements
1 parent c279bb4 commit 391754f

File tree

16 files changed

+230
-169
lines changed

16 files changed

+230
-169
lines changed

internal/devbox/nixprofile.go

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,79 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"os/exec"
87
"slices"
8+
"strings"
99

1010
"go.jetpack.io/devbox/internal/nix"
11+
"go.jetpack.io/devbox/internal/nix/nixprofile"
1112
)
1213

13-
func syncFlakeToProfile(ctx context.Context, flakePath, profilePath string) error {
14-
cmd := exec.CommandContext(ctx, "nix", "eval", "--json", flakePath+"#devShells."+nix.System()+".default.buildInputs")
15-
b, err := cmd.Output()
14+
func (d *Devbox) syncFlakeToProfile(ctx context.Context) error {
15+
profilePath, err := d.profilePath()
16+
if err != nil {
17+
return err
18+
}
19+
20+
// Get the build inputs (i.e. store paths) from the generated flake's devShell.
21+
buildInputPaths, err := nix.Eval(
22+
ctx,
23+
d.stderr,
24+
d.flakeDir()+"#devShells."+nix.System()+".default.buildInputs",
25+
"--json",
26+
)
1627
if err != nil {
1728
return fmt.Errorf("nix eval devShells: %v", err)
1829
}
1930
storePaths := []string{}
20-
if err := json.Unmarshal(b, &storePaths); err != nil {
21-
return fmt.Errorf("unmarshal store paths: %s: %v", b, err)
31+
if err := json.Unmarshal(buildInputPaths, &storePaths); err != nil {
32+
return fmt.Errorf("unmarshal store paths: %s: %v", buildInputPaths, err)
2233
}
2334

24-
listCmd := exec.CommandContext(ctx, "nix", "profile", "list", "--json", "--profile", profilePath)
25-
b, err = listCmd.Output()
35+
// Get the store-paths of the packages currently installed in the nix profile
36+
items, err := nixprofile.ProfileListItems(ctx, d.stderr, profilePath)
2637
if err != nil {
27-
return err
28-
}
29-
var profile struct {
30-
Elements []struct {
31-
StorePaths []string
32-
}
38+
return fmt.Errorf("nix profile list: %v", err)
3339
}
34-
if err := json.Unmarshal(b, &profile); err != nil {
35-
return fmt.Errorf("unmarshal profile: %v", err)
36-
}
37-
got := make([]string, 0, len(profile.Elements))
38-
for _, e := range profile.Elements {
39-
got = append(got, e.StorePaths...)
40+
got := make([]string, 0, len(items))
41+
for _, item := range items {
42+
got = append(got, item.StorePaths()...)
4043
}
4144

45+
// Diff the store paths and install/remove packages as needed
4246
add, remove := diffStorePaths(got, storePaths)
4347
if len(remove) > 0 {
44-
removeCmd := exec.CommandContext(ctx, "nix", "profile", "remove", "--profile", profilePath)
45-
removeCmd.Args = append(removeCmd.Args, remove...)
46-
if err := removeCmd.Run(); err != nil {
48+
packagesToRemove := make([]string, 0, len(remove))
49+
for _, p := range remove {
50+
storePath := nix.NewStorePathParts(p)
51+
packagesToRemove = append(packagesToRemove, fmt.Sprintf("%s@%s", storePath.Name, storePath.Version))
52+
}
53+
if len(packagesToRemove) == 1 {
54+
fmt.Fprintf(d.stderr, "Removing %s\n", strings.Join(packagesToRemove, ", "))
55+
} else {
56+
fmt.Fprintf(d.stderr, "Removing packages: %s\n", strings.Join(packagesToRemove, ", "))
57+
}
58+
59+
if err := nix.ProfileRemove(profilePath, remove...); err != nil {
4760
return err
4861
}
4962
}
5063
if len(add) > 0 {
51-
addCmd := exec.CommandContext(ctx, "nix", "profile", "install", "--profile", profilePath)
52-
addCmd.Args = append(addCmd.Args, add...)
53-
if err := addCmd.Run(); err != nil {
54-
return err
64+
total := len(add)
65+
for idx, addPath := range add {
66+
stepNum := idx + 1
67+
storePath := nix.NewStorePathParts(addPath)
68+
nameAndVersion := fmt.Sprintf("%s@%s", storePath.Name, storePath.Version)
69+
stepMsg := fmt.Sprintf("[%d/%d] %s", stepNum, total, nameAndVersion)
70+
71+
if err = nixprofile.ProfileInstall(ctx, &nixprofile.ProfileInstallArgs{
72+
CustomStepMessage: stepMsg,
73+
Installable: addPath,
74+
PackageName: storePath.Name,
75+
ProfilePath: profilePath,
76+
Writer: d.stderr,
77+
}); err != nil {
78+
return fmt.Errorf("error installing package %s: %w", addPath, err)
79+
}
5580
}
5681
}
5782
return nil
@@ -61,27 +86,27 @@ func diffStorePaths(got, want []string) (add, remove []string) {
6186
slices.Sort(got)
6287
slices.Sort(want)
6388

64-
var g, w int
89+
var gotIdx, wantIdx int
6590
for {
66-
if g >= len(got) {
67-
add = append(add, want[w:]...)
91+
if gotIdx >= len(got) {
92+
add = append(add, want[wantIdx:]...)
6893
break
6994
}
70-
if w >= len(want) {
71-
remove = append(remove, got[g:]...)
95+
if wantIdx >= len(want) {
96+
remove = append(remove, got[gotIdx:]...)
7297
break
7398
}
7499

75100
switch {
76-
case got[g] == want[w]:
77-
g++
78-
w++
79-
case got[g] < want[w]:
80-
remove = append(remove, got[g])
81-
g++
82-
case got[g] > want[w]:
83-
add = append(add, want[w])
84-
w++
101+
case got[gotIdx] == want[wantIdx]:
102+
gotIdx++
103+
wantIdx++
104+
case got[gotIdx] < want[wantIdx]:
105+
remove = append(remove, got[gotIdx])
106+
gotIdx++
107+
case got[gotIdx] > want[wantIdx]:
108+
add = append(add, want[wantIdx])
109+
wantIdx++
85110
}
86111
}
87112
return add, remove

internal/devbox/packages.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,7 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
292292
return err
293293
}
294294

295-
profile, err := d.profilePath()
296-
if err != nil {
297-
return err
298-
}
299-
if err := syncFlakeToProfile(ctx, d.flakeDir(), profile); err != nil {
295+
if err := d.syncFlakeToProfile(ctx); err != nil {
300296
return err
301297
}
302298

internal/devbox/util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func (d *Devbox) addDevboxUtilityPackage(ctx context.Context, pkg string) error
2626
return err
2727
}
2828

29-
return nixprofile.ProfileInstall(ctx, &nixprofile.ProfileInstallArgs{
29+
return nixprofile.ProfileInstallPackage(ctx, &nixprofile.ProfileInstallPackageArgs{
3030
Lockfile: d.lockfile,
3131
Package: pkg,
3232
ProfilePath: profilePath,

internal/devpkg/narinfo_cache.go

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ import (
44
"context"
55
"io"
66
"net/http"
7-
"strings"
87
"sync"
98
"time"
10-
"unicode"
119

1210
"github.com/pkg/errors"
1311
"go.jetpack.io/devbox/internal/boxcli/featureflag"
@@ -110,8 +108,8 @@ func (p *Package) fetchNarInfoStatus() (bool, error) {
110108
)
111109
}
112110

113-
pathParts := newStorePathParts(sysInfo.StorePath)
114-
reqURL := BinaryCache + "/" + pathParts.hash + ".narinfo"
111+
pathParts := nix.NewStorePathParts(sysInfo.StorePath)
112+
reqURL := BinaryCache + "/" + pathParts.Hash + ".narinfo"
115113
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
116114
defer cancel()
117115
req, err := http.NewRequestWithContext(ctx, http.MethodHead, reqURL, nil)
@@ -181,35 +179,3 @@ func (p *Package) sysInfoIfExists() (*lock.SystemInfo, error) {
181179
}
182180
return sysInfo, nil
183181
}
184-
185-
// storePath are the constituent parts of
186-
// /nix/store/<hash>-<name>-<version>
187-
//
188-
// This is a helper struct for analyzing the string representation
189-
type storePathParts struct {
190-
hash string
191-
name string
192-
version string
193-
}
194-
195-
// newStorePathParts splits a Nix store path into its hash, name and version
196-
// components in the same way that Nix does.
197-
//
198-
// See https://nixos.org/manual/nix/stable/language/builtins.html#builtins-parseDrvName
199-
func newStorePathParts(path string) storePathParts {
200-
path = strings.TrimPrefix(path, "/nix/store/")
201-
// path is now <hash>-<name>-<version
202-
203-
hash, name := path[:32], path[33:]
204-
dashIndex := 0
205-
for i, r := range name {
206-
if dashIndex != 0 && !unicode.IsLetter(r) {
207-
return storePathParts{hash: hash, name: name[:dashIndex], version: name[i:]}
208-
}
209-
dashIndex = 0
210-
if r == '-' {
211-
dashIndex = i
212-
}
213-
}
214-
return storePathParts{hash: hash, name: name}
215-
}

internal/devpkg/package_test.go

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -181,49 +181,6 @@ func TestHashFromNixPkgsURL(t *testing.T) {
181181
}
182182
}
183183

184-
func TestStorePathParts(t *testing.T) {
185-
testCases := []struct {
186-
storePath string
187-
expected storePathParts
188-
}{
189-
// simple case:
190-
{
191-
storePath: "/nix/store/cvrn84c1hshv2wcds7n1rhydi6lacqns-gnumake-4.4.1",
192-
expected: storePathParts{
193-
hash: "cvrn84c1hshv2wcds7n1rhydi6lacqns",
194-
name: "gnumake",
195-
version: "4.4.1",
196-
},
197-
},
198-
// the package name can have dashes:
199-
{
200-
storePath: "/nix/store/q2xdxsswjqmqcbax81pmazm367s7jzyb-cctools-binutils-darwin-wrapper-973.0.1",
201-
expected: storePathParts{
202-
hash: "q2xdxsswjqmqcbax81pmazm367s7jzyb",
203-
name: "cctools-binutils-darwin-wrapper",
204-
version: "973.0.1",
205-
},
206-
},
207-
// version is optional. This is an artificial example I constructed
208-
{
209-
storePath: "/nix/store/gfxwrd5nggc68pjj3g3jhlldim9rpg0p-coreutils",
210-
expected: storePathParts{
211-
hash: "gfxwrd5nggc68pjj3g3jhlldim9rpg0p",
212-
name: "coreutils",
213-
},
214-
},
215-
}
216-
217-
for _, testCase := range testCases {
218-
t.Run(testCase.storePath, func(t *testing.T) {
219-
parts := newStorePathParts(testCase.storePath)
220-
if parts != testCase.expected {
221-
t.Errorf("Expected %v, got %v", testCase.expected, parts)
222-
}
223-
})
224-
}
225-
}
226-
227184
func TestCanonicalName(t *testing.T) {
228185
tests := []struct {
229186
pkgName string

internal/devpkg/validation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func (p *Package) ValidateInstallsOnSystem() (bool, error) {
3838
if len(info) == 0 {
3939
return false, nil
4040
}
41-
if out, err := nix.Eval(u); err != nil &&
41+
if out, err := nix.RawEval(u); err != nil &&
4242
strings.Contains(string(out), "is not available on the requested hostPlatform") {
4343
return false, nil
4444
}

internal/nix/eval.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package nix
22

33
import (
4+
"context"
45
"encoding/json"
6+
"io"
57
"os"
68
"strconv"
79
)
@@ -46,14 +48,22 @@ func PackageKnownVulnerabilities(path string) []string {
4648
return vulnerabilities
4749
}
4850

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

59+
func Eval(ctx context.Context, w io.Writer, installable string, options ...string) ([]byte, error) {
60+
args := []string{"eval", installable}
61+
args = append(args, options...)
62+
cmd := commandContext(ctx, args...)
63+
cmd.Stderr = w
64+
return cmd.Output()
65+
}
66+
5767
func AllowInsecurePackages() {
5868
os.Setenv("NIXPKGS_ALLOW_INSECURE", "1")
5969
}

internal/nix/nixprofile/item.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,7 @@ func (i *NixProfileListItem) String() string {
8383
i.nixStorePaths,
8484
)
8585
}
86+
87+
func (i *NixProfileListItem) StorePaths() []string {
88+
return i.nixStorePaths
89+
}

0 commit comments

Comments
 (0)