Skip to content

Commit 4d5d77a

Browse files
authored
[lockfile] Add hidden lockfile for caching (#897)
## Summary This adds a hidden lock file for caching purposes. ### Motivation: This speeds up run/shell by only running `ensurePackagesAreInstalled` if config or nix profile manifest has changed. ### Why hidden? I'm not ready for this to be an official checked-in lock file. So for now it lives in `.devbox` and does not get committed to repo. ## How was it tested? In flakes example ran: ```bash devbox run echo 5 ``` and got a speed up of around 66%. Also tested ```bash cat .devbox/devbox.lock devbox add hello cat .devbox/devbox.lock # does not match original devbox run hello #success devbox rm hello cat .devbox/devbox.lock # matches original devbox run hello # error ```
1 parent 49be3c8 commit 4d5d77a

File tree

6 files changed

+132
-4
lines changed

6 files changed

+132
-4
lines changed

internal/cuecfg/cuecfg.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func Marshal(valuePtr any, extension string) ([]byte, error) {
2020
}
2121

2222
switch extension {
23-
case ".json":
23+
case ".json", ".lock":
2424
return MarshalJSON(valuePtr)
2525
case ".yml", ".yaml":
2626
return marshalYaml(valuePtr)
@@ -34,7 +34,7 @@ func Marshal(valuePtr any, extension string) ([]byte, error) {
3434

3535
func Unmarshal(data []byte, extension string, valuePtr any) error {
3636
switch extension {
37-
case ".json":
37+
case ".json", ".lock":
3838
return errors.WithStack(unmarshalJSON(data, valuePtr))
3939
case ".yml", ".yaml":
4040
return errors.WithStack(unmarshalYaml(data, valuePtr))
@@ -87,7 +87,7 @@ func WriteFile(path string, value any) error {
8787

8888
func IsSupportedExtension(ext string) bool {
8989
switch ext {
90-
case ".json", ".yml", ".yaml", ".toml", ".xml":
90+
case ".json", ".lock", ".yml", ".yaml", ".toml", ".xml":
9191
return true
9292
default:
9393
return false

internal/impl/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package impl
55

66
import (
7+
"crypto/sha256"
8+
"encoding/hex"
79
"io"
810
"net/http"
911
"net/url"
@@ -66,6 +68,15 @@ func (c *Config) MergedPackages(w io.Writer) []string {
6668
return lo.Uniq(append(c.RawPackages, global.RawPackages...))
6769
}
6870

71+
func (c *Config) Hash() (string, error) {
72+
json, err := cuecfg.MarshalJSON(c)
73+
if err != nil {
74+
return "", err
75+
}
76+
hash := sha256.Sum256(json)
77+
return hex.EncodeToString(hash[:]), nil
78+
}
79+
6980
func readConfig(path string) (*Config, error) {
7081
cfg := &Config{}
7182
return cfg, errors.WithStack(cuecfg.ParseFile(path, cfg))

internal/impl/devbox.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ func (d *Devbox) Config() *Config {
130130
return d.cfg
131131
}
132132

133+
func (d *Devbox) ConfigHash() (string, error) {
134+
return d.cfg.Hash()
135+
}
136+
133137
func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
134138
shellPlan := planner.GetShellPlan(d.projectDir, d.mergedPackages())
135139
shellPlan.DevPackages = pkgslice.Unique(append(d.localPackages(), shellPlan.DevPackages...))

internal/impl/packages.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/pkg/errors"
1414
"github.com/samber/lo"
1515
"go.jetpack.io/devbox/internal/debug"
16+
"go.jetpack.io/devbox/internal/lockfile"
1617
"go.jetpack.io/devbox/internal/nix"
1718
"go.jetpack.io/devbox/internal/plugin"
1819
"go.jetpack.io/devbox/internal/ux"
@@ -136,6 +137,17 @@ const (
136137
func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMode) error {
137138
defer trace.StartRegion(ctx, "ensurePackages").End()
138139

140+
lock, err := lockfile.Local(d)
141+
if err != nil {
142+
return err
143+
}
144+
145+
if upToDate, err := lock.IsUpToDate(); err != nil {
146+
return err
147+
} else if upToDate {
148+
return nil
149+
}
150+
139151
if err := d.generateShellFiles(); err != nil {
140152
return err
141153
}
@@ -147,7 +159,11 @@ func (d *Devbox) ensurePackagesAreInstalled(ctx context.Context, mode installMod
147159
return err
148160
}
149161

150-
return plugin.RemoveInvalidSymlinks(d.projectDir)
162+
if err := plugin.RemoveInvalidSymlinks(d.projectDir); err != nil {
163+
return err
164+
}
165+
166+
return lock.Update()
151167
}
152168

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

internal/lockfile/lockfile.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package lockfile
2+
3+
import (
4+
"errors"
5+
"os"
6+
"path/filepath"
7+
8+
"go.jetpack.io/devbox/internal/cuecfg"
9+
"go.jetpack.io/devbox/internal/nix"
10+
)
11+
12+
// localLockFile is a non-shared lock file that helps track the state of the
13+
// local devbox environment. It contains hashes that may not be the same across
14+
// machines (e.g. manifest hash).
15+
// When we do implement a shared lock file, it may contain some shared fields
16+
// with this one but not all.
17+
type localLockFile struct {
18+
project devboxProject
19+
ConfigHash string `json:"config_hash"`
20+
NixProfileManifestHash string `json:"nix_profile_manifest_hash"`
21+
}
22+
23+
func (l *localLockFile) equals(other *localLockFile) bool {
24+
return l.ConfigHash == other.ConfigHash &&
25+
l.NixProfileManifestHash == other.NixProfileManifestHash
26+
}
27+
28+
func (l *localLockFile) IsUpToDate() (bool, error) {
29+
newLock, err := forProject(l.project)
30+
if err != nil {
31+
return false, err
32+
}
33+
34+
return l.equals(newLock), nil
35+
}
36+
37+
func (l *localLockFile) Update() error {
38+
newLock, err := forProject(l.project)
39+
if err != nil {
40+
return err
41+
}
42+
*l = *newLock
43+
44+
return cuecfg.WriteFile(localLockFilePath(l.project), l)
45+
}
46+
47+
type devboxProject interface {
48+
ConfigHash() (string, error)
49+
ProjectDir() string
50+
}
51+
52+
func Local(project devboxProject) (*localLockFile, error) {
53+
lockFile := &localLockFile{project: project}
54+
err := cuecfg.ParseFile(localLockFilePath(project), lockFile)
55+
if errors.Is(err, os.ErrNotExist) {
56+
return lockFile, nil
57+
} else if err != nil {
58+
return nil, err
59+
}
60+
return lockFile, nil
61+
}
62+
63+
func forProject(project devboxProject) (*localLockFile, error) {
64+
configHash, err := project.ConfigHash()
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
nixHash, err := nix.ManifestHash(project.ProjectDir())
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
newLock := &localLockFile{
75+
project: project,
76+
ConfigHash: configHash,
77+
NixProfileManifestHash: nixHash,
78+
}
79+
80+
return newLock, nil
81+
}
82+
83+
func localLockFilePath(project devboxProject) string {
84+
return filepath.Join(project.ProjectDir(), ".devbox", "local.lock")
85+
}

internal/nix/profile.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package nix
33
import (
44
"bufio"
55
"bytes"
6+
"crypto/sha256"
7+
"encoding/hex"
68
"encoding/json"
79
"fmt"
810
"io"
@@ -331,6 +333,16 @@ func readManifest(profilePath string) (manifest, error) {
331333
return m, json.Unmarshal(data, &m)
332334
}
333335

336+
func ManifestHash(profileDir string) (string, error) {
337+
path := filepath.Join(profileDir, ProfilePath, "manifest.json")
338+
data, err := os.ReadFile(path)
339+
if err != nil && !os.IsNotExist(err) {
340+
return "", err
341+
}
342+
hash := sha256.Sum256(data)
343+
return hex.EncodeToString(hash[:]), nil
344+
}
345+
334346
func nextPriority(profilePath string) string {
335347
// error is ignored because it's ok if the file doesn't exist
336348
m, _ := readManifest(profilePath)

0 commit comments

Comments
 (0)