Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
46 changes: 46 additions & 0 deletions internal/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

"github.com/pkg/errors"
"github.com/tailscale/hujson"
"go.jetify.com/devbox/internal/cachehash"
"go.jetify.com/devbox/internal/devconfig/configfile"
"go.jetify.com/devbox/internal/devpkg"
"go.jetify.com/devbox/internal/lock"
Expand Down Expand Up @@ -54,6 +55,51 @@ type PluginOnlyData struct {
Source Includable
}

// Hash returns a hash of the plugin's config that also incorporates the
// contents of the files referenced by create_files. The embedded
// ConfigFile.Hash only covers the plugin.json itself, so without this a change
// to a create_files source file leaves the project's state hash unchanged and
// the file is never re-created in the virtenv. This matters most for local
// (path:) plugins under active development, whose source files can change
// without any edit to devbox.json or the plugin.json.
// See https://github.com/jetify-com/devbox/issues/2755
func (c *Config) Hash() (string, error) {
h, err := c.ConfigFile.Hash()
if err != nil {
return "", err
}
if c.Source == nil || len(c.CreateFiles) == 0 {
return h, nil
}

buf := bytes.Buffer{}
buf.WriteString(h)

// Iterate deterministically so the hash is stable across runs.
filePaths := make([]string, 0, len(c.CreateFiles))
for filePath := range c.CreateFiles {
filePaths = append(filePaths, filePath)
}
slices.Sort(filePaths)

for _, filePath := range filePaths {
buf.WriteString(filePath)
contentPath := c.CreateFiles[filePath]
if contentPath == "" {
continue
}
content, err := c.Source.FileContent(contentPath)
if err != nil {
// A missing or unreadable source file should not hard-fail the
// shell; the path is still part of the hash above.
continue
}
buf.Write(content)
}
Comment on lines +92 to +114

return cachehash.Bytes(buf.Bytes()), nil
}

func (c *Config) ProcessComposeYaml() (string, string) {
for file, contentPath := range c.CreateFiles {
if strings.HasSuffix(file, "process-compose.yaml") || strings.HasSuffix(file, "process-compose.yml") {
Expand Down
65 changes: 65 additions & 0 deletions internal/plugin/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2024 Jetify Inc. and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package plugin

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.jetify.com/devbox/nix/flake"
)

// TestConfigHashIncludesCreateFilesContent verifies that a plugin's hash
// changes when the content of a create_files source file changes. This is what
// makes local plugins under active development re-create their virtenv files
// when their source changes (https://github.com/jetify-com/devbox/issues/2755).
func TestConfigHashIncludesCreateFilesContent(t *testing.T) {
pluginDir := t.TempDir()
projectDir := t.TempDir()

pluginJSON := `{
"name": "testplugin",
"version": "0.0.1",
"create_files": {
"{{ .Virtenv }}/test.txt": "test.txt"
}
}`
require.NoError(t, os.WriteFile(
filepath.Join(pluginDir, "plugin.json"), []byte(pluginJSON), 0o644))
srcFile := filepath.Join(pluginDir, "test.txt")
require.NoError(t, os.WriteFile(srcFile, []byte("123"), 0o644))

cfg := localPluginConfigForTest(t, pluginDir, projectDir)

hash1, err := cfg.Hash()
require.NoError(t, err)

// Re-hashing without any change must be stable.
hash1Again, err := cfg.Hash()
require.NoError(t, err)
assert.Equal(t, hash1, hash1Again, "hash should be stable when nothing changes")

// Changing the create_files source content must change the hash so that the
// file gets re-created in the virtenv on the next shell.
require.NoError(t, os.WriteFile(srcFile, []byte("456"), 0o644))
hash2, err := cfg.Hash()
require.NoError(t, err)
assert.NotEqual(t, hash1, hash2,
"hash should change when create_files source content changes")
}

func localPluginConfigForTest(t *testing.T, pluginDir, projectDir string) *Config {
t.Helper()
ref, err := flake.ParseRef("path:" + pluginDir)
require.NoError(t, err)
localPlugin, err := newLocalPlugin(ref, projectDir)
require.NoError(t, err)
cfg, err := getConfigIfAny(localPlugin, projectDir)
require.NoError(t, err)
require.NotNil(t, cfg)
return cfg
}
Loading