Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
12 changes: 10 additions & 2 deletions internal/devbox/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
9 changes: 6 additions & 3 deletions internal/devconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package devconfig
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -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": [
Expand All @@ -93,7 +96,7 @@ func DefaultConfig() *Config {
}
}
}
`))
`, DefaultInitHook)))
if err != nil {
panic("default devbox.json is invalid: " + err.Error())
}
Expand Down
120 changes: 0 additions & 120 deletions internal/lock/local.go

This file was deleted.

28 changes: 14 additions & 14 deletions internal/lock/lockfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
116 changes: 116 additions & 0 deletions internal/lock/statehash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2023 Jetpack Technologies Inc and contributors. All rights reserved.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to git mv such files to preserve history and make reviewing the code changes easier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github shows the file as deleted, but I split the PR into 2 commits. If you take a look a7160e3 it contains the changes to the file.

// 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"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine for now, because our code gen logic switches on IsFish

Ideally, we'd have a shell enum value here (ref. the enum in devbox/shell.go) so that this is slightly more generalizable.

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
Comment on lines +50 to +71
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its confusing to call these lockfiles in the local variables. Why not stateHashFile and similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change

}

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{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newLock -> newHashFile

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")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the .lock pretty confusing usage here. Lockfiles in my mind represent a need to ensure that some particular state can be reproduced as done with package.lock or devbox.lock.

This feels to be more of a local-state-cache-representation or .devbox/state_cache_key.json, or simply .devbox/cache_key.json

WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure. I was not bold enough to change the name, but there's no downside really.

}

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))
}
5 changes: 5 additions & 0 deletions internal/shellgen/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding a comment to the effect of:

// skip adding init-hook recursion guard for the 
// default hook or any empty hook

As is, not obvious why the template below is being skipped.

Will also be a good idea to comment somewhere in the template that it will be skipped for default or empty hooks, in case we later introduce some other logic for init-hooks in the template files.

_, err = script.WriteString(body)
return errors.WithStack(err)
}

t, err := template.New(filename).Parse(tmpl)
if err != nil {
return errors.WithStack(err)
Expand Down