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/local.go b/internal/lock/local.go deleted file mode 100644 index 65b490eefc9..00000000000 --- a/internal/lock/local.go +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved. -// Use of this source code is governed by the license in the LICENSE file. - -package lock - -import ( - "errors" - "io/fs" - "path/filepath" - - "go.jetpack.io/devbox/internal/build" - "go.jetpack.io/devbox/internal/cachehash" - "go.jetpack.io/devbox/internal/cuecfg" -) - -// localLockFile 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"` - LockFileHash string `json:"lock_file_hash"` - NixProfileManifestHash string `json:"nix_profile_manifest_hash"` - NixPrintDevEnvHash string `json:"nix_print_dev_env_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 -} - -func isLocalUpToDate(project devboxProject) (bool, error) { - filesystemLock, err := readLocal(project) - if err != nil { - return false, err - } - newLock, err := forProject(project) - if err != nil { - return false, err - } - - return filesystemLock.equals(newLock), nil -} - -func updateLocal(project devboxProject) error { - l, err := readLocal(project) - if err != nil { - return err - } - newLock, err := forProject(l.project) - if err != nil { - return err - } - *l = *newLock - - return cuecfg.WriteFile(localLockFilePath(l.project), l) -} - -func readLocal(project devboxProject) (*localLockFile, error) { - lockFile := &localLockFile{project: project} - err := cuecfg.ParseFile(localLockFilePath(project), lockFile) - if errors.Is(err, fs.ErrNotExist) { - return lockFile, nil - } - if err != nil { - return nil, err - } - return lockFile, nil -} - -func forProject(project devboxProject) (*localLockFile, error) { - configHash, err := project.ConfigHash() - if err != nil { - return nil, err - } - - nixHash, err := manifestHash(project.ProjectDir()) - if err != nil { - return nil, err - } - - printDevEnvCacheHash, err := printDevEnvCacheHash(project.ProjectDir()) - if err != nil { - return nil, err - } - - lockfileHash, err := getLockfileHash(project) - if err != nil { - return nil, err - } - - newLock := &localLockFile{ - project: project, - ConfigHash: configHash, - DevboxVersion: build.Version, - LockFileHash: lockfileHash, - NixProfileManifestHash: nixHash, - NixPrintDevEnvHash: printDevEnvCacheHash, - } - - return newLock, nil -} - -func localLockFilePath(project devboxProject) string { - return filepath.Join(project.ProjectDir(), ".devbox", "local.lock") -} - -func manifestHash(profileDir string) (string, error) { - return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/nix/profile/default/manifest.json")) -} - -func printDevEnvCacheHash(profileDir string) (string, error) { - return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/.nix-print-dev-env-cache")) -} 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 new file mode 100644 index 00000000000..b0cd4eecf6e --- /dev/null +++ b/internal/lock/statehash.go @@ -0,0 +1,116 @@ +// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved. +// Use of this source code is governed by the license in the LICENSE file. + +package lock + +import ( + "errors" + "io/fs" + "path/filepath" + + "go.jetpack.io/devbox/internal/build" + "go.jetpack.io/devbox/internal/cachehash" + "go.jetpack.io/devbox/internal/cuecfg" +) + +// 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 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"` + NixPrintDevEnvHash string `json:"nix_print_dev_env_hash"` + NixProfileManifestHash string `json:"nix_profile_manifest_hash"` +} + +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 UpdateAndSaveStateHashFile(args UpdateStateHashFileArgs) error { + newLock, err := getCurrentStateHash(args) + if err != nil { + return err + } + + return cuecfg.WriteFile(stateHashFilePath(args.ProjectDir), newLock) +} + +func isStateUpToDate(args UpdateStateHashFileArgs) (bool, error) { + filesystemLock, err := readStateHashFile(args.ProjectDir) + if err != nil { + return false, err + } + newLock, err := getCurrentStateHash(args) + if err != nil { + return false, err + } + + return *filesystemLock == *newLock, nil +} + +func readStateHashFile(projectDir string) (*stateHashFile, error) { + lockFile := &stateHashFile{} + err := cuecfg.ParseFile(stateHashFilePath(projectDir), lockFile) + if errors.Is(err, fs.ErrNotExist) { + return lockFile, nil + } + if err != nil { + return nil, err + } + return lockFile, nil +} + +func getCurrentStateHash(args UpdateStateHashFileArgs) (*stateHashFile, error) { + nixHash, err := manifestHash(args.ProjectDir) + if err != nil { + return nil, err + } + + printDevEnvCacheHash, err := printDevEnvCacheHash(args.ProjectDir) + if err != nil { + return nil, err + } + + lockfileHash, err := getLockfileHash(args.ProjectDir) + if err != nil { + return nil, err + } + + newLock := &stateHashFile{ + ConfigHash: args.ConfigHash, + DevboxVersion: build.Version, + IsFish: args.IsFish, + LockFileHash: lockfileHash, + NixPrintDevEnvHash: printDevEnvCacheHash, + NixProfileManifestHash: nixHash, + } + + return newLock, nil +} + +func stateHashFilePath(projectDir string) string { + return filepath.Join(projectDir, ".devbox", "local.lock") +} + +func manifestHash(profileDir string) (string, error) { + return cachehash.JSONFile(filepath.Join(profileDir, ".devbox/nix/profile/default/manifest.json")) +} + +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)