From e71bd078a705b3580ed7ceb8fb7d815e019b1c01 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Wed, 31 Jan 2024 16:14:16 -0800 Subject: [PATCH 1/2] Rename --- internal/lock/{local.go => statehash.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename internal/lock/{local.go => statehash.go} (100%) diff --git a/internal/lock/local.go b/internal/lock/statehash.go similarity index 100% rename from internal/lock/local.go rename to internal/lock/statehash.go From a7160e394d78d3f5c53e54bbc233097c99eb8541 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Wed, 31 Jan 2024 16:15:34 -0800 Subject: [PATCH 2/2] [local-state] Local state and fish improvements --- internal/devbox/devbox.go | 2 +- internal/devbox/packages.go | 12 ++++- internal/devconfig/config.go | 9 ++-- internal/lock/lockfile.go | 28 ++++++------ internal/lock/statehash.go | 86 +++++++++++++++++------------------- internal/shellgen/scripts.go | 5 +++ 6 files changed, 77 insertions(+), 65 deletions(-) diff --git a/internal/devbox/devbox.go b/internal/devbox/devbox.go index 7efab85308a..4b1ffa19e4e 100644 --- a/internal/devbox/devbox.go +++ b/internal/devbox/devbox.go @@ -300,7 +300,7 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st var err error if opts.DontRecomputeEnvironment { - upToDate, _ := d.lockfile.IsUpToDateAndInstalled() + upToDate, _ := d.lockfile.IsUpToDateAndInstalled(isFishShell()) if !upToDate { cmd := `eval "$(devbox global shellenv --recompute)"` if isFishShell() { diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index d335163cbfe..6c7c7479fa0 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -244,7 +244,7 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er defer trace.StartRegion(ctx, "devboxEnsureStateIsUpToDate").End() defer debug.FunctionTimer().End() - upToDate, err := d.lockfile.IsUpToDateAndInstalled() + upToDate, err := d.lockfile.IsUpToDateAndInstalled(isFishShell()) if err != nil { return err } @@ -309,7 +309,15 @@ func (d *Devbox) updateLockfile(recomputeState bool) error { // If not, we leave the local.lock in a stale state, so that state is recomputed // on the next ensureStateIsUpToDate call with mode=ensure. if recomputeState { - return d.lockfile.UpdateAndSaveLocalLock() + configHash, err := d.ConfigHash() + if err != nil { + return err + } + return lock.UpdateAndSaveStateHashFile(lock.UpdateStateHashFileArgs{ + ProjectDir: d.projectDir, + ConfigHash: configHash, + IsFish: isFishShell(), + }) } return nil } diff --git a/internal/devconfig/config.go b/internal/devconfig/config.go index 7e8c10ec5aa..9160f3a6a72 100644 --- a/internal/devconfig/config.go +++ b/internal/devconfig/config.go @@ -6,6 +6,7 @@ package devconfig import ( "bytes" "encoding/json" + "fmt" "io" "net/http" "os" @@ -79,12 +80,14 @@ type Stage struct { Command string `json:"command"` } +const DefaultInitHook = "echo 'Welcome to devbox!' > /dev/null" + func DefaultConfig() *Config { - cfg, err := loadBytes([]byte(`{ + cfg, err := loadBytes([]byte(fmt.Sprintf(`{ "packages": [], "shell": { "init_hook": [ - "echo 'Welcome to devbox!' > /dev/null" + "%s" ], "scripts": { "test": [ @@ -93,7 +96,7 @@ func DefaultConfig() *Config { } } } -`)) +`, DefaultInitHook))) if err != nil { panic("default devbox.json is invalid: " + err.Error()) } diff --git a/internal/lock/lockfile.go b/internal/lock/lockfile.go index a6c6d81b91f..3227e428424 100644 --- a/internal/lock/lockfile.go +++ b/internal/lock/lockfile.go @@ -39,7 +39,7 @@ func GetFile(project devboxProject) (*File, error) { LockFileVersion: lockFileVersion, Packages: map[string]*Package{}, } - err := cuecfg.ParseFile(lockFilePath(project), lockFile) + err := cuecfg.ParseFile(lockFilePath(project.ProjectDir()), lockFile) if errors.Is(err, fs.ErrNotExist) { return lockFile, nil } @@ -98,11 +98,7 @@ func (f *File) ForceResolve(pkg string) (*Package, error) { } func (f *File) Save() error { - return cuecfg.WriteFile(lockFilePath(f.devboxProject), f) -} - -func (f *File) UpdateAndSaveLocalLock() error { - return updateLocal(f.devboxProject) + return cuecfg.WriteFile(lockFilePath(f.devboxProject.ProjectDir()), f) } func (f *File) LegacyNixpkgsPath(pkg string) string { @@ -152,13 +148,21 @@ func (f *File) Tidy() { // IsUpToDateAndInstalled returns true if the lockfile is up to date and the // local hashes match, which generally indicates all packages are correctly // installed and print-dev-env has been computed and cached. -func (f *File) IsUpToDateAndInstalled() (bool, error) { +func (f *File) IsUpToDateAndInstalled(isFish bool) (bool, error) { if dirty, err := f.isDirty(); err != nil { return false, err } else if dirty { return false, nil } - return isLocalUpToDate(f.devboxProject) + configHash, err := f.devboxProject.ConfigHash() + if err != nil { + return false, err + } + return isStateUpToDate(UpdateStateHashFileArgs{ + ProjectDir: f.devboxProject.ProjectDir(), + ConfigHash: configHash, + IsFish: isFish, + }) } func (f *File) isDirty() (bool, error) { @@ -177,12 +181,8 @@ func (f *File) isDirty() (bool, error) { return currentHash != filesystemHash, nil } -func lockFilePath(project devboxProject) string { - return filepath.Join(project.ProjectDir(), "devbox.lock") -} - -func getLockfileHash(project devboxProject) (string, error) { - return cachehash.JSONFile(lockFilePath(project)) +func lockFilePath(projectDir string) string { + return filepath.Join(projectDir, "devbox.lock") } func ResolveRunXPackage(ctx context.Context, pkg string) (types.PkgRef, error) { diff --git a/internal/lock/statehash.go b/internal/lock/statehash.go index 65b490eefc9..b0cd4eecf6e 100644 --- a/internal/lock/statehash.go +++ b/internal/lock/statehash.go @@ -13,58 +13,55 @@ import ( "go.jetpack.io/devbox/internal/cuecfg" ) -// localLockFile is a non-shared lock file that helps track the state of the +// stateHashFile is a non-shared lock file that helps track the state of the // local devbox environment. It contains hashes that may not be the same across // machines (e.g. manifest hash). // When we do implement a shared lock file, it may contain some shared fields // with this one but not all. -type localLockFile struct { - project devboxProject - ConfigHash string `json:"config_hash"` - DevboxVersion string `json:"devbox_version"` +type stateHashFile struct { + ConfigHash string `json:"config_hash"` + DevboxVersion string `json:"devbox_version"` + // fish has different generated scripts so we need to recompute them if user + // changes shell. + IsFish bool `json:"is_fish"` LockFileHash string `json:"lock_file_hash"` - NixProfileManifestHash string `json:"nix_profile_manifest_hash"` NixPrintDevEnvHash string `json:"nix_print_dev_env_hash"` + NixProfileManifestHash string `json:"nix_profile_manifest_hash"` } -func (l *localLockFile) equals(other *localLockFile) bool { - return l.ConfigHash == other.ConfigHash && - l.LockFileHash == other.LockFileHash && - l.NixProfileManifestHash == other.NixProfileManifestHash && - l.NixPrintDevEnvHash == other.NixPrintDevEnvHash && - l.DevboxVersion == other.DevboxVersion +type UpdateStateHashFileArgs struct { + ProjectDir string + ConfigHash string + // IsFish is an arg because in the future we may allow the user + // to specify shell in devbox.json which should be passed in here. + IsFish bool } -func isLocalUpToDate(project devboxProject) (bool, error) { - filesystemLock, err := readLocal(project) - if err != nil { - return false, err - } - newLock, err := forProject(project) +func UpdateAndSaveStateHashFile(args UpdateStateHashFileArgs) error { + newLock, err := getCurrentStateHash(args) if err != nil { - return false, err + return err } - return filesystemLock.equals(newLock), nil + return cuecfg.WriteFile(stateHashFilePath(args.ProjectDir), newLock) } -func updateLocal(project devboxProject) error { - l, err := readLocal(project) +func isStateUpToDate(args UpdateStateHashFileArgs) (bool, error) { + filesystemLock, err := readStateHashFile(args.ProjectDir) if err != nil { - return err + return false, err } - newLock, err := forProject(l.project) + newLock, err := getCurrentStateHash(args) if err != nil { - return err + return false, err } - *l = *newLock - return cuecfg.WriteFile(localLockFilePath(l.project), l) + return *filesystemLock == *newLock, nil } -func readLocal(project devboxProject) (*localLockFile, error) { - lockFile := &localLockFile{project: project} - err := cuecfg.ParseFile(localLockFilePath(project), lockFile) +func readStateHashFile(projectDir string) (*stateHashFile, error) { + lockFile := &stateHashFile{} + err := cuecfg.ParseFile(stateHashFilePath(projectDir), lockFile) if errors.Is(err, fs.ErrNotExist) { return lockFile, nil } @@ -74,41 +71,36 @@ func readLocal(project devboxProject) (*localLockFile, error) { return lockFile, nil } -func forProject(project devboxProject) (*localLockFile, error) { - configHash, err := project.ConfigHash() - if err != nil { - return nil, err - } - - nixHash, err := manifestHash(project.ProjectDir()) +func getCurrentStateHash(args UpdateStateHashFileArgs) (*stateHashFile, error) { + nixHash, err := manifestHash(args.ProjectDir) if err != nil { return nil, err } - printDevEnvCacheHash, err := printDevEnvCacheHash(project.ProjectDir()) + printDevEnvCacheHash, err := printDevEnvCacheHash(args.ProjectDir) if err != nil { return nil, err } - lockfileHash, err := getLockfileHash(project) + lockfileHash, err := getLockfileHash(args.ProjectDir) if err != nil { return nil, err } - newLock := &localLockFile{ - project: project, - ConfigHash: configHash, + newLock := &stateHashFile{ + ConfigHash: args.ConfigHash, DevboxVersion: build.Version, + IsFish: args.IsFish, LockFileHash: lockfileHash, - NixProfileManifestHash: nixHash, NixPrintDevEnvHash: printDevEnvCacheHash, + NixProfileManifestHash: nixHash, } return newLock, nil } -func localLockFilePath(project devboxProject) string { - return filepath.Join(project.ProjectDir(), ".devbox", "local.lock") +func stateHashFilePath(projectDir string) string { + return filepath.Join(projectDir, ".devbox", "local.lock") } func manifestHash(profileDir string) (string, error) { @@ -118,3 +110,7 @@ func manifestHash(profileDir string) (string, error) { func printDevEnvCacheHash(profileDir string) (string, error) { return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/.nix-print-dev-env-cache")) } + +func getLockfileHash(projectDir string) (string, error) { + return cachehash.JSONFile(lockFilePath(projectDir)) +} diff --git a/internal/shellgen/scripts.go b/internal/shellgen/scripts.go index cb6982ced1e..3853d8b7be5 100644 --- a/internal/shellgen/scripts.go +++ b/internal/shellgen/scripts.go @@ -110,6 +110,11 @@ func writeInitHookFile(devbox devboxer, body, tmpl, filename string) (err error) } defer script.Close() // best effort: close file + if body == devconfig.DefaultInitHook || strings.TrimSpace(body) == "" { + _, err = script.WriteString(body) + return errors.WithStack(err) + } + t, err := template.New(filename).Parse(tmpl) if err != nil { return errors.WithStack(err)