diff --git a/internal/devconfig/config.go b/internal/devconfig/config.go index 1151deb14a3..e67c7558d5b 100644 --- a/internal/devconfig/config.go +++ b/internal/devconfig/config.go @@ -141,8 +141,15 @@ func Find(path string) (*Config, error) { // searchDir looks for a config file in dir. It does not search parent // directories. +// +// It first looks for devbox.json directly in dir, and then falls back to +// dir/.config/devbox.json. The latter lets projects keep their root directory +// tidy by storing the config under .config. See jetify-com/devbox#2792. func searchDir(dir string) (*Config, error) { - try := []string{configfile.DefaultName} + try := []string{ + configfile.DefaultName, + filepath.Join(configfile.ConfigSubdir, configfile.DefaultName), + } for _, name := range try { path := filepath.Join(dir, name) slog.Debug("trying config file", "path", path) diff --git a/internal/devconfig/config_test.go b/internal/devconfig/config_test.go index 9f747796156..9b22060aabe 100644 --- a/internal/devconfig/config_test.go +++ b/internal/devconfig/config_test.go @@ -95,6 +95,63 @@ func TestOpenError(t *testing.T) { }) } +// TestConfigSubdir verifies that Devbox discovers a config file stored in a +// .config subdirectory, so that projects can keep devbox.json out of their +// root directory. See jetify-com/devbox#2792. +func TestConfigSubdir(t *testing.T) { + t.Run("Open", func(t *testing.T) { + root, _, _ := mkNestedDirs(t) + configDir := mkConfigSubdir(t, root) + if _, err := Init(configDir); err != nil { + t.Fatalf("Init(%q) error: %v", configDir, err) + } + + cfg, err := Open(root) + if err != nil { + t.Fatalf("Open(%q) error: %v", root, err) + } + gotDir := filepath.Dir(cfg.Root.AbsRootPath) + if gotDir != configDir { + t.Errorf("filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q", gotDir, configDir) + } + }) + t.Run("RootConfigTakesPrecedence", func(t *testing.T) { + root, _, _ := mkNestedDirs(t) + if _, err := Init(root); err != nil { + t.Fatalf("Init(%q) error: %v", root, err) + } + configDir := mkConfigSubdir(t, root) + if _, err := Init(configDir); err != nil { + t.Fatalf("Init(%q) error: %v", configDir, err) + } + + cfg, err := Open(root) + if err != nil { + t.Fatalf("Open(%q) error: %v", root, err) + } + gotDir := filepath.Dir(cfg.Root.AbsRootPath) + if gotDir != root { + t.Errorf("filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q", gotDir, root) + } + }) + t.Run("FindFromChildDir", func(t *testing.T) { + root, child, _ := mkNestedDirs(t) + configDir := mkConfigSubdir(t, root) + if _, err := Init(configDir); err != nil { + t.Fatalf("Init(%q) error: %v", configDir, err) + } + + cfg, err := Find(child) + if err != nil { + t.Fatalf("Find(%q) error: %v", child, err) + } + gotDir := filepath.Dir(cfg.Root.AbsRootPath) + if gotDir != configDir { + t.Errorf("filepath.Dir(cfg.Root.AbsRootPath) = %q, want %q", gotDir, configDir) + } + }) +} + func TestFind(t *testing.T) { t.Run("StartInSameDir", func(t *testing.T) { root, child, _ := mkNestedDirs(t) @@ -297,6 +354,19 @@ func mkNestedDirs(t *testing.T) (root, child, nested string) { return root, child, nested } +// mkConfigSubdir creates a .config subdirectory inside dir and returns its +// path, for tests that store devbox.json under .config. +func mkConfigSubdir(t *testing.T, dir string) string { + t.Helper() + + configDir := filepath.Join(dir, configfile.ConfigSubdir) + perm := fs.FileMode(0o777) + if err := os.MkdirAll(configDir, perm); err != nil { + t.Fatalf("os.MkdirAll(%q, %O) error: %v", configDir, perm, err) + } + return configDir +} + func TestAliases(t *testing.T) { dir := t.TempDir() cfgJSON := `{ diff --git a/internal/devconfig/configfile/file.go b/internal/devconfig/configfile/file.go index 6857f037ce7..16ef70558af 100644 --- a/internal/devconfig/configfile/file.go +++ b/internal/devconfig/configfile/file.go @@ -21,6 +21,12 @@ import ( const ( DefaultName = "devbox.json" + + // ConfigSubdir is a subdirectory that Devbox also searches for a config + // file. It lets projects keep their root directory tidy by placing + // devbox.json inside a .config directory (e.g. .config/devbox.json) + // instead of at the top level. See jetify-com/devbox#2792. + ConfigSubdir = ".config" ) // ConfigFile defines a devbox environment as JSON.