Skip to content

Commit 4f50c91

Browse files
authored
[local-state] Local state and fish improvements (#1765)
## Summary A few fish and local state improvements: * [fish] Don't add recursion protection if init hook is empty or default. Recursion protection is shell-specific so it may cause issues in some edge cases (e.g. if someone uses a `shellenv --init-hooks` output on a different shell). * [fish] Incorporate fish into local state hash. This means that if a user changes shell, we consider the local state stale. This is needed because we generate different files (e.g. shellrc) depending on the shell. * [local-state] Rename a bunch of stuff in local.go to be more accureately named ( `stateHash` instead of `localLock`) * [local-state] Decoupled `stateHash` from lock file. These are different and should not depend on each other. They are still in the same package, but we can change that in the future. * Removed the devbox interface from `statehash`. As @gcurtis warned us many moons ago, interfaces like this make code harder to follow. ## How was it tested? Tested empty/default init hooks not having recursion protection: ```bash devbox init devbox run echo foo cat .devbox/gen/scripts/.hooks.sh ``` Tested local state with fish ```bash devbox run echo 5 cat .devbox/local.lock SHELL=fish devbox run echo 5 cat .devbox/local.lock ```
1 parent 5b7a14b commit 4f50c91

File tree

7 files changed

+152
-140
lines changed

7 files changed

+152
-140
lines changed

internal/devbox/devbox.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st
300300
var err error
301301

302302
if opts.DontRecomputeEnvironment {
303-
upToDate, _ := d.lockfile.IsUpToDateAndInstalled()
303+
upToDate, _ := d.lockfile.IsUpToDateAndInstalled(isFishShell())
304304
if !upToDate {
305305
cmd := `eval "$(devbox global shellenv --recompute)"`
306306
if isFishShell() {

internal/devbox/packages.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er
244244
defer trace.StartRegion(ctx, "devboxEnsureStateIsUpToDate").End()
245245
defer debug.FunctionTimer().End()
246246

247-
upToDate, err := d.lockfile.IsUpToDateAndInstalled()
247+
upToDate, err := d.lockfile.IsUpToDateAndInstalled(isFishShell())
248248
if err != nil {
249249
return err
250250
}
@@ -309,7 +309,15 @@ func (d *Devbox) updateLockfile(recomputeState bool) error {
309309
// If not, we leave the local.lock in a stale state, so that state is recomputed
310310
// on the next ensureStateIsUpToDate call with mode=ensure.
311311
if recomputeState {
312-
return d.lockfile.UpdateAndSaveLocalLock()
312+
configHash, err := d.ConfigHash()
313+
if err != nil {
314+
return err
315+
}
316+
return lock.UpdateAndSaveStateHashFile(lock.UpdateStateHashFileArgs{
317+
ProjectDir: d.projectDir,
318+
ConfigHash: configHash,
319+
IsFish: isFishShell(),
320+
})
313321
}
314322
return nil
315323
}

internal/devconfig/config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package devconfig
66
import (
77
"bytes"
88
"encoding/json"
9+
"fmt"
910
"io"
1011
"net/http"
1112
"os"
@@ -79,12 +80,14 @@ type Stage struct {
7980
Command string `json:"command"`
8081
}
8182

83+
const DefaultInitHook = "echo 'Welcome to devbox!' > /dev/null"
84+
8285
func DefaultConfig() *Config {
83-
cfg, err := loadBytes([]byte(`{
86+
cfg, err := loadBytes([]byte(fmt.Sprintf(`{
8487
"packages": [],
8588
"shell": {
8689
"init_hook": [
87-
"echo 'Welcome to devbox!' > /dev/null"
90+
"%s"
8891
],
8992
"scripts": {
9093
"test": [
@@ -93,7 +96,7 @@ func DefaultConfig() *Config {
9396
}
9497
}
9598
}
96-
`))
99+
`, DefaultInitHook)))
97100
if err != nil {
98101
panic("default devbox.json is invalid: " + err.Error())
99102
}

internal/lock/local.go

Lines changed: 0 additions & 120 deletions
This file was deleted.

internal/lock/lockfile.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func GetFile(project devboxProject) (*File, error) {
3939
LockFileVersion: lockFileVersion,
4040
Packages: map[string]*Package{},
4141
}
42-
err := cuecfg.ParseFile(lockFilePath(project), lockFile)
42+
err := cuecfg.ParseFile(lockFilePath(project.ProjectDir()), lockFile)
4343
if errors.Is(err, fs.ErrNotExist) {
4444
return lockFile, nil
4545
}
@@ -98,11 +98,7 @@ func (f *File) ForceResolve(pkg string) (*Package, error) {
9898
}
9999

100100
func (f *File) Save() error {
101-
return cuecfg.WriteFile(lockFilePath(f.devboxProject), f)
102-
}
103-
104-
func (f *File) UpdateAndSaveLocalLock() error {
105-
return updateLocal(f.devboxProject)
101+
return cuecfg.WriteFile(lockFilePath(f.devboxProject.ProjectDir()), f)
106102
}
107103

108104
func (f *File) LegacyNixpkgsPath(pkg string) string {
@@ -152,13 +148,21 @@ func (f *File) Tidy() {
152148
// IsUpToDateAndInstalled returns true if the lockfile is up to date and the
153149
// local hashes match, which generally indicates all packages are correctly
154150
// installed and print-dev-env has been computed and cached.
155-
func (f *File) IsUpToDateAndInstalled() (bool, error) {
151+
func (f *File) IsUpToDateAndInstalled(isFish bool) (bool, error) {
156152
if dirty, err := f.isDirty(); err != nil {
157153
return false, err
158154
} else if dirty {
159155
return false, nil
160156
}
161-
return isLocalUpToDate(f.devboxProject)
157+
configHash, err := f.devboxProject.ConfigHash()
158+
if err != nil {
159+
return false, err
160+
}
161+
return isStateUpToDate(UpdateStateHashFileArgs{
162+
ProjectDir: f.devboxProject.ProjectDir(),
163+
ConfigHash: configHash,
164+
IsFish: isFish,
165+
})
162166
}
163167

164168
func (f *File) isDirty() (bool, error) {
@@ -177,12 +181,8 @@ func (f *File) isDirty() (bool, error) {
177181
return currentHash != filesystemHash, nil
178182
}
179183

180-
func lockFilePath(project devboxProject) string {
181-
return filepath.Join(project.ProjectDir(), "devbox.lock")
182-
}
183-
184-
func getLockfileHash(project devboxProject) (string, error) {
185-
return cachehash.JSONFile(lockFilePath(project))
184+
func lockFilePath(projectDir string) string {
185+
return filepath.Join(projectDir, "devbox.lock")
186186
}
187187

188188
func ResolveRunXPackage(ctx context.Context, pkg string) (types.PkgRef, error) {

internal/lock/statehash.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 lock
5+
6+
import (
7+
"errors"
8+
"io/fs"
9+
"path/filepath"
10+
11+
"go.jetpack.io/devbox/internal/build"
12+
"go.jetpack.io/devbox/internal/cachehash"
13+
"go.jetpack.io/devbox/internal/cuecfg"
14+
)
15+
16+
// stateHashFile is a non-shared lock file that helps track the state of the
17+
// local devbox environment. It contains hashes that may not be the same across
18+
// machines (e.g. manifest hash).
19+
// When we do implement a shared lock file, it may contain some shared fields
20+
// with this one but not all.
21+
type stateHashFile struct {
22+
ConfigHash string `json:"config_hash"`
23+
DevboxVersion string `json:"devbox_version"`
24+
// fish has different generated scripts so we need to recompute them if user
25+
// changes shell.
26+
IsFish bool `json:"is_fish"`
27+
LockFileHash string `json:"lock_file_hash"`
28+
NixPrintDevEnvHash string `json:"nix_print_dev_env_hash"`
29+
NixProfileManifestHash string `json:"nix_profile_manifest_hash"`
30+
}
31+
32+
type UpdateStateHashFileArgs struct {
33+
ProjectDir string
34+
ConfigHash string
35+
// IsFish is an arg because in the future we may allow the user
36+
// to specify shell in devbox.json which should be passed in here.
37+
IsFish bool
38+
}
39+
40+
func UpdateAndSaveStateHashFile(args UpdateStateHashFileArgs) error {
41+
newLock, err := getCurrentStateHash(args)
42+
if err != nil {
43+
return err
44+
}
45+
46+
return cuecfg.WriteFile(stateHashFilePath(args.ProjectDir), newLock)
47+
}
48+
49+
func isStateUpToDate(args UpdateStateHashFileArgs) (bool, error) {
50+
filesystemLock, err := readStateHashFile(args.ProjectDir)
51+
if err != nil {
52+
return false, err
53+
}
54+
newLock, err := getCurrentStateHash(args)
55+
if err != nil {
56+
return false, err
57+
}
58+
59+
return *filesystemLock == *newLock, nil
60+
}
61+
62+
func readStateHashFile(projectDir string) (*stateHashFile, error) {
63+
lockFile := &stateHashFile{}
64+
err := cuecfg.ParseFile(stateHashFilePath(projectDir), lockFile)
65+
if errors.Is(err, fs.ErrNotExist) {
66+
return lockFile, nil
67+
}
68+
if err != nil {
69+
return nil, err
70+
}
71+
return lockFile, nil
72+
}
73+
74+
func getCurrentStateHash(args UpdateStateHashFileArgs) (*stateHashFile, error) {
75+
nixHash, err := manifestHash(args.ProjectDir)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
printDevEnvCacheHash, err := printDevEnvCacheHash(args.ProjectDir)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
lockfileHash, err := getLockfileHash(args.ProjectDir)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
newLock := &stateHashFile{
91+
ConfigHash: args.ConfigHash,
92+
DevboxVersion: build.Version,
93+
IsFish: args.IsFish,
94+
LockFileHash: lockfileHash,
95+
NixPrintDevEnvHash: printDevEnvCacheHash,
96+
NixProfileManifestHash: nixHash,
97+
}
98+
99+
return newLock, nil
100+
}
101+
102+
func stateHashFilePath(projectDir string) string {
103+
return filepath.Join(projectDir, ".devbox", "local.lock")
104+
}
105+
106+
func manifestHash(profileDir string) (string, error) {
107+
return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/nix/profile/default/manifest.json"))
108+
}
109+
110+
func printDevEnvCacheHash(profileDir string) (string, error) {
111+
return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/.nix-print-dev-env-cache"))
112+
}
113+
114+
func getLockfileHash(projectDir string) (string, error) {
115+
return cachehash.JSONFile(lockFilePath(projectDir))
116+
}

internal/shellgen/scripts.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ func writeInitHookFile(devbox devboxer, body, tmpl, filename string) (err error)
110110
}
111111
defer script.Close() // best effort: close file
112112

113+
if body == devconfig.DefaultInitHook || strings.TrimSpace(body) == "" {
114+
_, err = script.WriteString(body)
115+
return errors.WithStack(err)
116+
}
117+
113118
t, err := template.New(filename).Parse(tmpl)
114119
if err != nil {
115120
return errors.WithStack(err)

0 commit comments

Comments
 (0)