Skip to content

Commit 344dc6c

Browse files
authored
lock: add support for locking stdenv + flakerefs (#2465)
Make the `stdenv` (and other flakerefs) lockable and updateable. This makes it possible to update the stdenv with a regular `devbox update` and simplifies the logic for how the stdenv commit is chosen: 1. If there's no stdenv flakeref in devbox.lock, resolve github:NixOS/nixpkgs/nixpkgs-unstable to a locked ref and store it in the lockfile. 2. Otherwise, use the ref in the lockfile.
1 parent 5fa89f8 commit 344dc6c

File tree

20 files changed

+244
-110
lines changed

20 files changed

+244
-110
lines changed

internal/devbox/devbox.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"go.jetpack.io/devbox/internal/shellgen"
4747
"go.jetpack.io/devbox/internal/telemetry"
4848
"go.jetpack.io/devbox/internal/ux"
49+
"go.jetpack.io/devbox/nix/flake"
4950
)
5051

5152
const (
@@ -202,8 +203,14 @@ func (d *Devbox) ConfigHash() (string, error) {
202203
return cachehash.Bytes(buf.Bytes()), nil
203204
}
204205

205-
func (d *Devbox) NixPkgsCommitHash() string {
206-
return d.cfg.NixPkgsCommitHash()
206+
func (d *Devbox) Stdenv() flake.Ref {
207+
return flake.Ref{
208+
Type: flake.TypeGitHub,
209+
Owner: "NixOS",
210+
Repo: "nixpkgs",
211+
Ref: "nixpkgs-unstable",
212+
Rev: d.cfg.NixPkgsCommitHash(),
213+
}
207214
}
208215

209216
func (d *Devbox) Generate(ctx context.Context) error {

internal/devbox/packages.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"go.jetpack.io/devbox/internal/setup"
2929
"go.jetpack.io/devbox/internal/shellgen"
3030
"go.jetpack.io/devbox/internal/telemetry"
31+
"go.jetpack.io/devbox/nix/flake"
3132
"go.jetpack.io/pkg/auth"
3233

3334
"go.jetpack.io/devbox/internal/boxcli/usererr"
@@ -101,10 +102,17 @@ func (d *Devbox) Add(ctx context.Context, pkgsNames []string, opts devopt.AddOpt
101102
// This means it didn't validate and we don't want to fallback to legacy
102103
// Just propagate the error.
103104
return err
104-
} else if _, err := nix.Search(d.lockfile.LegacyNixpkgsPath(pkg.Raw)); err != nil {
105-
// This means it looked like a devbox package or attribute path, but we
106-
// could not find it in search or in the legacy nixpkgs path.
107-
return usererr.New("Package %s not found", pkg.Raw)
105+
} else {
106+
installable := flake.Installable{
107+
Ref: d.lockfile.Stdenv(),
108+
AttrPath: pkg.Raw,
109+
}
110+
_, err := nix.Search(installable.String())
111+
if err != nil {
112+
// This means it looked like a devbox package or attribute path, but we
113+
// could not find it in search or in the legacy nixpkgs path.
114+
return usererr.New("Package %s not found", pkg.Raw)
115+
}
108116
}
109117

110118
ux.Finfof(d.stderr, "Adding package %q to devbox.json\n", packageNameForConfig)

internal/devbox/update.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ func (d *Devbox) Update(ctx context.Context, opts devopt.UpdateOpts) error {
6565
}
6666
}
6767

68+
if err := d.updateStdenv(); err != nil {
69+
return err
70+
}
6871
if err := d.ensureStateIsUpToDate(ctx, update); err != nil {
6972
return err
7073
}
@@ -103,6 +106,15 @@ func (d *Devbox) inputsToUpdate(
103106
return pkgsToUpdate, nil
104107
}
105108

109+
func (d *Devbox) updateStdenv() error {
110+
err := d.lockfile.Remove(d.Stdenv().String())
111+
if err != nil {
112+
return err
113+
}
114+
d.lockfile.Stdenv() // will re-resolve the stdenv flake
115+
return nil
116+
}
117+
106118
func (d *Devbox) updateDevboxPackage(pkg *devpkg.Package) error {
107119
resolved, err := d.lockfile.FetchResolvedPackage(pkg.Raw)
108120
if err != nil {

internal/devconfig/configfile/file.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,8 @@ func (c *ConfigFile) Equals(other *ConfigFile) bool {
9494
}
9595

9696
func (c *ConfigFile) NixPkgsCommitHash() string {
97-
// The commit hash for nixpkgs-unstable on 2023-10-25 from status.nixos.org
98-
const DefaultNixpkgsCommit = "75a52265bda7fd25e06e3a67dee3f0354e73243c"
99-
100-
if c == nil || c.Nixpkgs == nil || c.Nixpkgs.Commit == "" {
101-
return DefaultNixpkgsCommit
97+
if c == nil || c.Nixpkgs == nil {
98+
return ""
10299
}
103100
return c.Nixpkgs.Commit
104101
}

internal/devpkg/package.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,13 @@ func newPackage(raw string, isInstallable func() bool, locker lock.Locker) *Pack
151151
return pkg
152152
}
153153

154-
// We currently don't lock flake references in devbox.lock, so there's
155-
// nothing to resolve.
156-
pkg.resolve = sync.OnceValue(func() error { return nil })
154+
pkg.resolve = sync.OnceValue(func() error {
155+
// Don't lock flakes that are local paths.
156+
if parsed.Ref.Type == flake.TypePath {
157+
return nil
158+
}
159+
return resolve(pkg)
160+
})
157161
pkg.setInstallable(parsed, locker.ProjectDir())
158162
pkg.outputs = outputs{selectedNames: strings.Split(parsed.Outputs, ",")}
159163
pkg.Patch = pkgNeedsPatch(pkg.CanonicalName(), configfile.PatchAuto)

internal/devpkg/package_test.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/samber/lo"
1313
"go.jetpack.io/devbox/internal/lock"
1414
"go.jetpack.io/devbox/internal/nix"
15+
"go.jetpack.io/devbox/nix/flake"
1516
)
1617

1718
const nixCommitHash = "hsdafkhsdafhas"
@@ -108,12 +109,13 @@ func (l *lockfile) ProjectDir() string {
108109
return l.projectDir
109110
}
110111

111-
func (l *lockfile) LegacyNixpkgsPath(pkg string) string {
112-
return fmt.Sprintf(
113-
"github:NixOS/nixpkgs/%s#%s",
114-
nixCommitHash,
115-
pkg,
116-
)
112+
func (l *lockfile) Stdenv() flake.Ref {
113+
return flake.Ref{
114+
Type: flake.TypeGitHub,
115+
Owner: "NixOS",
116+
Repo: "nixpkgs",
117+
Rev: nixCommitHash,
118+
}
117119
}
118120

119121
func (l *lockfile) Get(pkg string) *lock.Package {
@@ -128,7 +130,10 @@ func (l *lockfile) Resolve(pkg string) (*lock.Package, error) {
128130
return &lock.Package{Resolved: pkg}, nil
129131
default:
130132
return &lock.Package{
131-
Resolved: l.LegacyNixpkgsPath(pkg),
133+
Resolved: flake.Installable{
134+
Ref: l.Stdenv(),
135+
AttrPath: pkg,
136+
}.String(),
132137
}, nil
133138
}
134139
}

internal/lock/interfaces.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33

44
package lock
55

6+
import "go.jetpack.io/devbox/nix/flake"
7+
68
type devboxProject interface {
79
ConfigHash() (string, error)
8-
NixPkgsCommitHash() string
10+
Stdenv() flake.Ref
911
AllPackageNamesIncludingRemovedTriggerPackages() []string
1012
ProjectDir() string
1113
}
1214

1315
type Locker interface {
1416
Get(string) *Package
15-
LegacyNixpkgsPath(string) string
17+
Stdenv() flake.Ref
1618
ProjectDir() string
1719
Resolve(string) (*Package, error)
1820
}

internal/lock/lockfile.go

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ package lock
55

66
import (
77
"context"
8-
"fmt"
98
"io/fs"
9+
"maps"
1010
"path/filepath"
11+
"slices"
1112
"strings"
1213

1314
"github.com/pkg/errors"
14-
"github.com/samber/lo"
1515
"go.jetpack.io/devbox/internal/cachehash"
1616
"go.jetpack.io/devbox/internal/devpkg/pkgtype"
1717
"go.jetpack.io/devbox/internal/nix"
1818
"go.jetpack.io/devbox/internal/searcher"
19+
"go.jetpack.io/devbox/nix/flake"
1920
"go.jetpack.io/pkg/runx/impl/types"
2021

2122
"go.jetpack.io/devbox/internal/cuecfg"
@@ -74,25 +75,32 @@ func (f *File) Remove(pkgs ...string) error {
7475
// This avoids writing values that may need to be removed in case of error.
7576
func (f *File) Resolve(pkg string) (*Package, error) {
7677
entry, hasEntry := f.Packages[pkg]
78+
if hasEntry && entry.Resolved != "" {
79+
return f.Packages[pkg], nil
80+
}
7781

78-
if !hasEntry || entry.Resolved == "" {
79-
locked := &Package{}
80-
var err error
81-
if _, _, versioned := searcher.ParseVersionedPackage(pkg); pkgtype.IsRunX(pkg) || versioned {
82-
locked, err = f.FetchResolvedPackage(pkg)
83-
if err != nil {
84-
return nil, err
85-
}
86-
} else if IsLegacyPackage(pkg) {
87-
// These are legacy packages without a version. Resolve to nixpkgs with
88-
// whatever hash is in the devbox.json
89-
locked = &Package{
90-
Resolved: f.LegacyNixpkgsPath(pkg),
91-
Source: nixpkgSource,
92-
}
82+
locked := &Package{}
83+
_, _, versioned := searcher.ParseVersionedPackage(pkg)
84+
if pkgtype.IsRunX(pkg) || versioned || pkgtype.IsFlake(pkg) {
85+
resolved, err := f.FetchResolvedPackage(pkg)
86+
if err != nil {
87+
return nil, err
88+
}
89+
if resolved != nil {
90+
locked = resolved
91+
}
92+
} else if IsLegacyPackage(pkg) {
93+
// These are legacy packages without a version. Resolve to nixpkgs with
94+
// whatever hash is in the devbox.json
95+
locked = &Package{
96+
Resolved: flake.Installable{
97+
Ref: f.Stdenv(),
98+
AttrPath: pkg,
99+
}.String(),
100+
Source: nixpkgSource,
93101
}
94-
f.Packages[pkg] = locked
95102
}
103+
f.Packages[pkg] = locked
96104

97105
return f.Packages[pkg], nil
98106
}
@@ -133,12 +141,17 @@ func (f *File) Save() error {
133141
return cuecfg.WriteFile(lockFilePath(f.devboxProject.ProjectDir()), f)
134142
}
135143

136-
func (f *File) LegacyNixpkgsPath(pkg string) string {
137-
return fmt.Sprintf(
138-
"github:NixOS/nixpkgs/%s#%s",
139-
f.NixPkgsCommitHash(),
140-
pkg,
141-
)
144+
func (f *File) Stdenv() flake.Ref {
145+
unlocked := f.devboxProject.Stdenv()
146+
pkg, err := f.Resolve(unlocked.String())
147+
if err != nil {
148+
return unlocked
149+
}
150+
ref, err := flake.ParseRef(pkg.Resolved)
151+
if err != nil {
152+
return unlocked
153+
}
154+
return ref
142155
}
143156

144157
func (f *File) Get(pkg string) *Package {
@@ -174,10 +187,11 @@ func IsLegacyPackage(pkg string) bool {
174187
// Tidy ensures that the lockfile has the set of packages corresponding to the devbox.json config.
175188
// It gets rid of older packages that are no longer needed.
176189
func (f *File) Tidy() {
177-
f.Packages = lo.PickByKeys(
178-
f.Packages,
179-
f.devboxProject.AllPackageNamesIncludingRemovedTriggerPackages(),
180-
)
190+
keep := f.devboxProject.AllPackageNamesIncludingRemovedTriggerPackages()
191+
keep = append(keep, f.devboxProject.Stdenv().String())
192+
maps.DeleteFunc(f.Packages, func(key string, pkg *Package) bool {
193+
return !slices.Contains(keep, key)
194+
})
181195
}
182196

183197
// IsUpToDateAndInstalled returns true if the lockfile is up to date and the

internal/lock/resolve.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.jetpack.io/devbox/internal/nix"
1919
"go.jetpack.io/devbox/internal/redact"
2020
"go.jetpack.io/devbox/internal/searcher"
21+
"go.jetpack.io/devbox/nix/flake"
2122
"golang.org/x/sync/errgroup"
2223
)
2324

@@ -29,7 +30,17 @@ import (
2930
// to update because it would be slow and wasteful.
3031
func (f *File) FetchResolvedPackage(pkg string) (*Package, error) {
3132
if pkgtype.IsFlake(pkg) {
32-
return nil, nil
33+
installable, err := flake.ParseInstallable(pkg)
34+
if err != nil {
35+
return nil, fmt.Errorf("package %q: %v", pkg, err)
36+
}
37+
installable.Ref, err = lockFlake(context.TODO(), installable.Ref)
38+
if err != nil {
39+
return nil, err
40+
}
41+
return &Package{
42+
Resolved: installable.String(),
43+
}, nil
3344
}
3445

3546
name, version, _ := searcher.ParseVersionedPackage(pkg)
@@ -194,3 +205,24 @@ func buildLockSystemInfos(pkg *searcher.PackageVersion) (map[string]*SystemInfo,
194205
}
195206
return sysInfos, nil
196207
}
208+
209+
func lockFlake(ctx context.Context, ref flake.Ref) (flake.Ref, error) {
210+
if ref.Locked() {
211+
return ref, nil
212+
}
213+
214+
// Nix requires a NAR hash for GitHub flakes to be locked. A Devbox lock
215+
// file is a bit more lenient and only requires a revision so that we
216+
// don't need to download the nixpkgs source for cached packages. If the
217+
// search index is ever able to return the NAR hash then we can remove
218+
// this check.
219+
if ref.Type == flake.TypeGitHub && (ref.Rev != "") {
220+
return ref, nil
221+
}
222+
223+
meta, err := nix.ResolveFlake(ctx, ref)
224+
if err != nil {
225+
return ref, err
226+
}
227+
return meta.Locked, nil
228+
}

internal/nix/build.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ type BuildArgs struct {
2020

2121
func Build(ctx context.Context, args *BuildArgs, installables ...string) error {
2222
defer debug.FunctionTimer().End()
23+
24+
FixInstallableArgs(installables)
25+
2326
// --impure is required for allowUnfreeEnv/allowInsecureEnv to work.
2427
cmd := command("build", "--impure")
2528
cmd.Args = appendArgs(cmd.Args, args.Flags)

0 commit comments

Comments
 (0)