Skip to content

Commit e40cb0a

Browse files
authored
[update] Implement devbox update (#978)
## Summary Implements `devbox update [pkg]...` It basically just re-resolves version. If there's a new version that satisfies constraints, it installs it and removes the previous one. Other improvements: * implemented `findPackageByName` which allows us to use `devbox update go` instead of `devbox update [email protected]`. In follow up we can also use this for `devbox rm` * flake.nix optimization: If there are no dev packages (this is a legacy planner concept that is almost never used) and we add at least one nixpkgs flake input, we use that input for mkShell. This avoids having to download an additional nixpkgs. ## How was it tested? * Manually modified lockfile to point to older versions and ran `devbox update` and `devbox update go` * Inspected flake.nix * Inspected nix profile
1 parent 5aab8d4 commit e40cb0a

File tree

14 files changed

+225
-60
lines changed

14 files changed

+225
-60
lines changed

devbox.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ type Devbox interface {
4949
StartServices(ctx context.Context, services ...string) error
5050
StopServices(ctx context.Context, allProjects bool, services ...string) error
5151
ListServices(ctx context.Context) error
52+
53+
Update(ctx context.Context, pkgs ...string) error
5254
}
5355

5456
// Open opens a devbox by reading the config file in dir.

devbox.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"packages": [
3-
"go_1_20",
4-
"golangci-lint",
5-
"actionlint"
3+
4+
5+
66
],
77
"env": {
88
"PATH": "$PATH:$PWD/dist"

devbox.lock

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
{
22
"lockfile_version": "1",
33
"packages": {
4-
"actionlint": {
5-
"last_modified": "",
6-
"resolved": "github:NixOS/nixpkgs/3364b5b117f65fe1ce65a3cdd5612a078a3b31e3#actionlint",
7-
"version": ""
4+
"actionlint@1.6.23": {
5+
"last_modified": "2023-03-31T22:52:29Z",
6+
"resolved": "github:NixOS/nixpkgs/242246ee1e58f54d2322227fc5eef53b4a616a31#actionlint",
7+
"version": "1.6.23"
88
},
9-
"go_1_20": {
10-
"last_modified": "",
11-
"resolved": "github:NixOS/nixpkgs/3364b5b117f65fe1ce65a3cdd5612a078a3b31e3#go_1_20",
12-
"version": ""
9+
10+
"last_modified": "2023-03-31T22:52:29Z",
11+
"resolved": "github:NixOS/nixpkgs/242246ee1e58f54d2322227fc5eef53b4a616a31#go",
12+
"version": "1.20.2"
1313
},
14-
"golangci-lint": {
15-
"last_modified": "",
16-
"resolved": "github:NixOS/nixpkgs/3364b5b117f65fe1ce65a3cdd5612a078a3b31e3#golangci-lint",
17-
"version": ""
14+
"golangci-lint@1.52.2": {
15+
"last_modified": "2023-03-31T22:52:29Z",
16+
"resolved": "github:NixOS/nixpkgs/242246ee1e58f54d2322227fc5eef53b4a616a31#golangci-lint",
17+
"version": "1.52.2"
1818
}
1919
}
20-
}
20+
}

internal/boxcli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func RootCmd() *cobra.Command {
6161
command.AddCommand(setupCmd())
6262
command.AddCommand(shellCmd())
6363
command.AddCommand(shellEnvCmd())
64+
command.AddCommand(updateCmd())
6465
command.AddCommand(versionCmd())
6566
// Preview commands
6667
command.AddCommand(cloudCmd())

internal/boxcli/update.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"github.com/pkg/errors"
8+
"github.com/spf13/cobra"
9+
"go.jetpack.io/devbox"
10+
)
11+
12+
type updateCmdFlags struct {
13+
config configFlags
14+
}
15+
16+
func updateCmd() *cobra.Command {
17+
flags := &updateCmdFlags{}
18+
19+
command := &cobra.Command{
20+
Use: "update [pkg]...",
21+
Short: "Updates packages in your devbox",
22+
Long: "Updates one, many, or all packages in your devbox. " +
23+
"If no packages are specified, all packages will be updated. " +
24+
"Only updates versioned packages (e.g. `[email protected]`), not packages that are pinned to a nix channel (e.g. `python3`)",
25+
PreRunE: ensureNixInstalled,
26+
RunE: func(cmd *cobra.Command, args []string) error {
27+
return updateCmdFunc(cmd, args, flags)
28+
},
29+
}
30+
31+
flags.config.register(command)
32+
return command
33+
}
34+
35+
func updateCmdFunc(
36+
cmd *cobra.Command,
37+
args []string,
38+
flags *updateCmdFlags,
39+
) error {
40+
box, err := devbox.Open(flags.config.path, cmd.ErrOrStderr())
41+
if err != nil {
42+
return errors.WithStack(err)
43+
}
44+
45+
return box.Update(cmd.Context(), args...)
46+
}

internal/impl/devbox.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,19 @@ func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
150150
shellPlan.FlakeInputs = d.flakeInputs()
151151

152152
nixpkgsInfo := plansdk.GetNixpkgsInfo(d.cfg.Nixpkgs.Commit)
153+
154+
// This is an optimization. If there are no dev packages (which we only use
155+
// for php/haskell planners) we can use nixpkgs from one of the flakes.
156+
// That saves us from downloading nixpkgs an additional time for mkShell.
157+
if len(shellPlan.DevPackages) == 0 {
158+
for _, input := range shellPlan.FlakeInputs {
159+
if input.IsNixpkgs() {
160+
nixpkgsInfo = plansdk.GetNixpkgsInfo(input.HashFromNixPkgsURL())
161+
break
162+
}
163+
}
164+
}
165+
153166
shellPlan.NixpkgsInfo = nixpkgsInfo
154167

155168
return shellPlan, nil
@@ -942,6 +955,27 @@ func (d *Devbox) packages() []string {
942955
return d.cfg.Packages
943956
}
944957

958+
func (d *Devbox) findPackageByName(name string) (string, error) {
959+
results := []string{}
960+
for _, pkg := range d.cfg.Packages {
961+
i := nix.InputFromString(pkg, d.lockfile)
962+
if i.String() == name || i.CanonicalName() == name {
963+
results = append(results, pkg)
964+
}
965+
}
966+
if len(results) > 1 {
967+
return "", usererr.New(
968+
"found multiple packages with name %s: %s. Please specify version",
969+
name,
970+
results,
971+
)
972+
}
973+
if len(results) == 0 {
974+
return "", usererr.New("no package found with name %s", name)
975+
}
976+
return results[0], nil
977+
}
978+
945979
// configEnvs takes the computed env variables (nix + plugin) and adds env
946980
// variables defined in Config. It also parses variables in config
947981
// that are referenced by $VAR or ${VAR} and replaces them with

internal/impl/update.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package impl
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"go.jetpack.io/devbox/internal/lock"
8+
)
9+
10+
func (d *Devbox) Update(ctx context.Context, pkgs ...string) error {
11+
var pkgsToUpdate []string
12+
for _, pkg := range pkgs {
13+
found, err := d.findPackageByName(pkg)
14+
if err != nil {
15+
return err
16+
}
17+
pkgsToUpdate = append(pkgsToUpdate, found)
18+
}
19+
if len(pkgsToUpdate) == 0 {
20+
pkgsToUpdate = d.packages()
21+
}
22+
23+
for _, pkg := range pkgsToUpdate {
24+
if !lock.IsVersionedPackage(pkg) {
25+
fmt.Fprintf(d.writer, "Skipping %s because it is not a versioned package\n", pkg)
26+
continue
27+
}
28+
existing := d.lockfile.Entry(pkg)
29+
newEntry, err := d.lockfile.ForceResolve(pkg)
30+
if err != nil {
31+
return err
32+
}
33+
if existing != nil && existing.Version != newEntry.Version {
34+
fmt.Fprintf(d.writer, "Updating %s %s -> %s\n", pkg, existing.Version, newEntry.Version)
35+
if err := d.removePackagesFromProfile(ctx, []string{pkg}); err != nil {
36+
return err
37+
}
38+
} else if existing == nil {
39+
fmt.Fprintf(d.writer, "Resolved %s to %[1]s %[2]s\n", pkg, newEntry.Resolved)
40+
} else {
41+
fmt.Fprintf(d.writer, "Already up-to-date %s %s\n", pkg, existing.Version)
42+
}
43+
}
44+
45+
// TODO(landau): Improve output
46+
return d.ensurePackagesAreInstalled(ctx, ensure)
47+
}

internal/lock/lockfile.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ type File struct {
2121
devboxProject
2222
resolver
2323

24-
LockFileVersion string `json:"lockfile_version"`
25-
Packages map[string]Package `json:"packages"`
24+
LockFileVersion string `json:"lockfile_version"`
25+
Packages map[string]*Package `json:"packages"`
2626
}
2727

2828
type Package struct {
@@ -37,7 +37,7 @@ func GetFile(project devboxProject, resolver resolver) (*File, error) {
3737
resolver: resolver,
3838

3939
LockFileVersion: lockFileVersion,
40-
Packages: map[string]Package{},
40+
Packages: map[string]*Package{},
4141
}
4242
err := cuecfg.ParseFile(lockFilePath(project), lockFile)
4343
if errors.Is(err, fs.ErrNotExist) {
@@ -51,7 +51,7 @@ func GetFile(project devboxProject, resolver resolver) (*File, error) {
5151

5252
func (l *File) Add(pkgs ...string) error {
5353
for _, p := range pkgs {
54-
if l.IsVersionedPackage(p) {
54+
if IsVersionedPackage(p) {
5555
if _, err := l.Resolve(p); err != nil {
5656
return err
5757
}
@@ -67,15 +67,14 @@ func (l *File) Remove(pkgs ...string) error {
6767
return l.Update()
6868
}
6969

70-
func (l *File) Resolve(pkg string) (string, error) {
71-
if _, ok := l.Packages[pkg]; !ok {
70+
func (l *File) Resolve(pkg string) (*Package, error) {
71+
if entry, ok := l.Packages[pkg]; !ok || entry.Resolved == "" {
7272
var locked *Package
7373
var err error
74-
if l.IsVersionedPackage(pkg) {
75-
name, version, _ := strings.Cut(pkg, "@")
76-
locked, err = l.resolver.Resolve(name, version)
74+
if IsVersionedPackage(pkg) {
75+
locked, err = l.resolver.Resolve(pkg)
7776
if err != nil {
78-
return "", err
77+
return nil, err
7978
}
8079
} else {
8180
// These are legacy packages without a version. Resolve to nixpkgs with
@@ -88,13 +87,22 @@ func (l *File) Resolve(pkg string) (string, error) {
8887
),
8988
}
9089
}
91-
l.Packages[pkg] = *locked
90+
l.Packages[pkg] = locked
9291
if err := l.Update(); err != nil {
93-
return "", err
92+
return nil, err
9493
}
9594
}
9695

97-
return l.Packages[pkg].Resolved, nil
96+
return l.Packages[pkg], nil
97+
}
98+
99+
func (l *File) ForceResolve(pkg string) (*Package, error) {
100+
delete(l.Packages, pkg)
101+
return l.Resolve(pkg)
102+
}
103+
104+
func (l *File) Entry(pkg string) *Package {
105+
return l.Packages[pkg]
98106
}
99107

100108
func (l *File) Update() error {
@@ -106,6 +114,11 @@ func (l *File) Update() error {
106114
return cuecfg.WriteFile(lockFilePath(l), l)
107115
}
108116

117+
func IsVersionedPackage(pkg string) bool {
118+
name, version, found := strings.Cut(pkg, "@")
119+
return found && name != "" && version != ""
120+
}
121+
109122
func lockFilePath(project devboxProject) string {
110123
return filepath.Join(project.ProjectDir(), "devbox.lock")
111124
}

internal/lock/resolvers.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ type devboxProject interface {
1010
}
1111

1212
type resolver interface {
13-
IsVersionedPackage(pkg string) bool
14-
Resolve(pkg, version string) (*Package, error)
13+
Resolve(pkg string) (*Package, error)
1514
}
1615

1716
type Locker interface {
1817
devboxProject
19-
Resolve(pkg string) (string, error)
18+
resolver
2019
}

internal/nix/input.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,24 @@ func (i *Input) Name() string {
6969

7070
func (i *Input) URLForInput() string {
7171
if i.IsDevboxPackage() {
72-
resolved, err := i.lockfile.Resolve(i.String())
72+
entry, err := i.lockfile.Resolve(i.String())
7373
if err != nil {
7474
panic(err)
7575
// TODO(landau): handle error
7676
}
77-
withoutFragment, _, _ := strings.Cut(resolved, "#")
77+
withoutFragment, _, _ := strings.Cut(entry.Resolved, "#")
7878
return withoutFragment
7979
}
8080
return i.urlWithoutFragment()
8181
}
8282

8383
func (i *Input) URLForInstall() (string, error) {
8484
if i.IsDevboxPackage() {
85-
return i.lockfile.Resolve(i.String())
85+
entry, err := i.lockfile.Resolve(i.String())
86+
if err != nil {
87+
return "", err
88+
}
89+
return entry.Resolved, nil
8690
}
8791
attrPath, err := i.PackageAttributePath()
8892
if err != nil {
@@ -97,11 +101,11 @@ func (i *Input) URLForInstall() (string, error) {
97101
func (i *Input) PackageAttributePath() (string, error) {
98102
var infos map[string]*Info
99103
if i.IsDevboxPackage() {
100-
path, err := i.lockfile.Resolve(i.String())
104+
entry, err := i.lockfile.Resolve(i.String())
101105
if err != nil {
102106
return "", err
103107
}
104-
infos = search(path)
108+
infos = search(entry.Resolved)
105109
} else {
106110
infos = search(i.String())
107111
}
@@ -158,7 +162,11 @@ func (i *Input) hash() string {
158162

159163
func (i *Input) validateExists() (bool, error) {
160164
if i.IsDevboxPackage() {
161-
return searcher.Exists(i.canonicalName(), i.version())
165+
version := i.version()
166+
if version == "" && i.isVersioned() {
167+
return false, usererr.New("No version specified for %q.", i.Path)
168+
}
169+
return searcher.Exists(i.CanonicalName(), version)
162170
}
163171
info, err := i.PackageAttributePath()
164172
return info != "", err
@@ -185,9 +193,9 @@ func (i *Input) equals(other *Input) bool {
185193
return name == otherName
186194
}
187195

188-
// canonicalName returns the name of the package without the version
196+
// CanonicalName returns the name of the package without the version
189197
// it only applies to devbox packages
190-
func (i *Input) canonicalName() string {
198+
func (i *Input) CanonicalName() string {
191199
if !i.IsDevboxPackage() {
192200
return ""
193201
}
@@ -205,6 +213,10 @@ func (i *Input) version() string {
205213
return version
206214
}
207215

216+
func (i *Input) isVersioned() bool {
217+
return i.IsDevboxPackage() && strings.Contains(i.Path, "@")
218+
}
219+
208220
func (i *Input) hashFromNiPkgsURL() string {
209221
return HashFromNixPkgsURL(i.URLForInput())
210222
}

0 commit comments

Comments
 (0)