Skip to content

Add --envrc-dir flag to allow specifying location of direnv config #2629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
70 changes: 66 additions & 4 deletions internal/boxcli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package boxcli
import (
"cmp"
"fmt"
"path/filepath"
"regexp"

"github.com/pkg/errors"
Expand All @@ -24,6 +25,7 @@ type generateCmdFlags struct {
force bool
printEnvrcContent bool
rootUser bool
envrcDir string // only used by generate direnv command
}

type generateDockerfileCmdFlags struct {
Expand Down Expand Up @@ -151,6 +153,15 @@ func direnvCmd() *cobra.Command {
// this command marks a flag as hidden. Error handling for it is not necessary.
_ = command.Flags().MarkHidden("print-envrc")

// --envrc-dir allows users to specify a directory where the .envrc file should be generated
// separately from the devbox config directory. Without this flag, the .envrc file
// will be generated in the same directory as the devbox config file (i.e., either the current
// directory or the directory specified by --config). This is useful for users who want to keep
// their .envrc and devbox config files in different locations.
command.Flags().StringVar(
&flags.envrcDir, "envrc-dir", "", "path to directory where the .envrc file should be generated. "+
"If not specified, the .envrc file will be generated in the current working directory.")

flags.config.register(command)
return command
}
Expand Down Expand Up @@ -266,13 +277,32 @@ func runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
}

func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
// --print-envrc is used within the .envrc file and therefore doesn't make sense to also
// use it with --envrc-dir, which specifies a directory where the .envrc file should be generated.
if flags.printEnvrcContent && flags.envrcDir != "" {
return usererr.New(
"Cannot use --print-envrc with --envrc-dir. " +
"Use --envrc-dir to specify the directory where the .envrc file should be generated.")
}

// Determine the directories for .envrc and config
configDir, envrcDir, err := determineDirenvDirs(flags.config.path, flags.envrcDir)
if err != nil {
return errors.WithStack(err)
}

generateOpts := devopt.EnvrcOpts{
EnvrcDir: envrcDir,
ConfigDir: configDir,
EnvFlags: devopt.EnvFlags(flags.envFlag),
}

if flags.printEnvrcContent {
return devbox.PrintEnvrcContent(
cmd.OutOrStdout(), devopt.EnvFlags(flags.envFlag))
return devbox.PrintEnvrcContent(cmd.OutOrStdout(), generateOpts)
}

box, err := devbox.Open(&devopt.Opts{
Dir: flags.config.path,
Dir: filepath.Join(envrcDir, configDir),
Environment: flags.config.environment,
Stderr: cmd.ErrOrStderr(),
})
Expand All @@ -281,5 +311,37 @@ func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
}

return box.GenerateEnvrcFile(
cmd.Context(), flags.force, devopt.EnvFlags(flags.envFlag))
cmd.Context(), flags.force, generateOpts)
}

// Returns cononical paths for configDir and envrcDir. Both locations are relative to the current
// working directory when provided to this function. However, since the config file will ultimately
// be relative to the .envrc file, we need to determine the relative path from envrcDir to configDir.
func determineDirenvDirs(configDir, envrcDir string) (string, string, error) {
// If envrcDir is not specified, we will use the configDir as the location for .envrc. This is
// for backward compatibility (prior to the --envrc-dir flag being introduced).
if envrcDir == "" {
return "", configDir, nil
}

// If no configDir is specified, it will be assumed to be in the same directory as the .envrc file
// which means we can just return an empty configDir.
if configDir == "" {
return "", envrcDir, nil
}

relativeConfigDir, err := filepath.Rel(envrcDir, configDir)
if err != nil {
return "", "", errors.Wrapf(err, "failed to determine relative path from %s to %s", envrcDir, configDir)
}

// If the relative path is ".", it means configDir is the same as envrcDir. Leaving it as "."
// will result in the .envrc containing "--config .", which is fine, but unnecessary and also
// a change from the previous behavior. So we will return an empty string for relativeConfigDir
// which will result in the .envrc file not containing the "--config" flag at all.
if relativeConfigDir == "." {
relativeConfigDir = ""
}

return relativeConfigDir, envrcDir, nil
}
20 changes: 10 additions & 10 deletions internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,21 +527,21 @@ func (d *Devbox) GenerateDockerfile(ctx context.Context, generateOpts devopt.Gen
}))
}

func PrintEnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
return generate.EnvrcContent(w, envFlags)
func PrintEnvrcContent(w io.Writer, opts devopt.EnvrcOpts) error {
return generate.EnvrcContent(w, opts)
}

// GenerateEnvrcFile generates a .envrc file that makes direnv integration convenient
func (d *Devbox) GenerateEnvrcFile(ctx context.Context, force bool, envFlags devopt.EnvFlags) error {
func (d *Devbox) GenerateEnvrcFile(ctx context.Context, force bool, opts devopt.EnvrcOpts) error {
ctx, task := trace.NewTask(ctx, "devboxGenerateEnvrc")
defer task.End()

envrcfilePath := filepath.Join(d.projectDir, ".envrc")
envrcfilePath := filepath.Join(opts.EnvrcDir, ".envrc")
filesExist := fileutil.Exists(envrcfilePath)
if !force && filesExist {
return usererr.New(
"A .envrc is already present in the current directory. " +
"Remove it or use --force to overwrite it.",
"A .envrc is already present in %q. Remove it or use --force to overwrite it.",
opts.EnvrcDir,
)
}

Expand All @@ -551,18 +551,18 @@ func (d *Devbox) GenerateEnvrcFile(ctx context.Context, force bool, envFlags dev
}

// .envrc file creation
err := generate.CreateEnvrc(ctx, d.projectDir, envFlags)
err := generate.CreateEnvrc(ctx, opts)
if err != nil {
return errors.WithStack(err)
}
ux.Fsuccessf(d.stderr, "generated .envrc file\n")
ux.Fsuccessf(d.stderr, "generated .envrc file in %q.\n", opts.EnvrcDir)
if cmdutil.Exists("direnv") {
cmd := exec.Command("direnv", "allow")
cmd := exec.Command("direnv", "allow", opts.EnvrcDir)
err := cmd.Run()
if err != nil {
return errors.WithStack(err)
}
ux.Fsuccessf(d.stderr, "ran `direnv allow`\n")
ux.Fsuccessf(d.stderr, "ran `direnv allow %s`\n", opts.EnvrcDir)
}
return nil
}
Expand Down
6 changes: 6 additions & 0 deletions internal/devbox/devopt/devboxopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ type EnvFlags struct {
EnvFile string
}

type EnvrcOpts struct {
EnvFlags
EnvrcDir string
ConfigDir string
}

type PullboxOpts struct {
Overwrite bool
URL string
Expand Down
16 changes: 9 additions & 7 deletions internal/devbox/generate/devcontainer_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,32 +140,33 @@ func (g *Options) CreateDevcontainer(ctx context.Context) error {
return err
}

func CreateEnvrc(ctx context.Context, path string, envFlags devopt.EnvFlags) error {
func CreateEnvrc(ctx context.Context, opts devopt.EnvrcOpts) error {
defer trace.StartRegion(ctx, "createEnvrc").End()

// create .envrc file
file, err := os.Create(filepath.Join(path, ".envrc"))
file, err := os.Create(filepath.Join(opts.EnvrcDir, ".envrc"))
if err != nil {
return err
}
defer file.Close()

flags := []string{}

if len(envFlags.EnvMap) > 0 {
for k, v := range envFlags.EnvMap {
if len(opts.EnvMap) > 0 {
for k, v := range opts.EnvMap {
flags = append(flags, fmt.Sprintf("--env %s=%s", k, v))
}
}
if envFlags.EnvFile != "" {
flags = append(flags, fmt.Sprintf("--env-file %s", envFlags.EnvFile))
if opts.EnvFile != "" {
flags = append(flags, fmt.Sprintf("--env-file %s", opts.EnvFile))
}

t := template.Must(template.ParseFS(tmplFS, "tmpl/envrc.tmpl"))

// write content into file
return t.Execute(file, map[string]string{
"Flags": strings.Join(flags, " "),
"Dir": opts.ConfigDir,
})
}

Expand Down Expand Up @@ -219,7 +220,7 @@ func (g *Options) getDevcontainerContent() *devcontainerObject {
return devcontainerContent
}

func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
func EnvrcContent(w io.Writer, envFlags devopt.EnvrcOpts) error {
tmplName := "envrcContent.tmpl"
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
envFlag := ""
Expand All @@ -231,5 +232,6 @@ func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
return t.Execute(w, map[string]string{
"EnvFlag": envFlag,
"EnvFile": envFlags.EnvFile,
"Dir": envFlags.EnvrcDir,
})
}
2 changes: 1 addition & 1 deletion internal/devbox/generate/tmpl/envrc.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Automatically sets up your devbox environment whenever you cd into this
# directory via our direnv integration:

eval "$(devbox generate direnv --print-envrc{{ if .Flags}} {{ .Flags }}{{ end }})"
eval "$(devbox generate direnv --print-envrc{{ if .Flags}} {{ .Flags }}{{ end }}{{ if .Dir }} --config {{ .Dir -}}{{ end }})"

# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
# for more details
5 changes: 3 additions & 2 deletions internal/devbox/generate/tmpl/envrcContent.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{{define "DirPrefix"}}{{ if .Dir }}{{ .Dir }}/{{ end }}{{end}}
use_devbox() {
watch_file devbox.json devbox.lock
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }})"
watch_file {{template "DirPrefix" .}}devbox.json {{template "DirPrefix" .}}devbox.lock
eval "$(devbox shellenv --init-hook --install --no-refresh-alias {{ if .EnvFlag }}{{ .EnvFlag }}{{ end }} {{- if .Dir }}--config {{ .Dir -}}{{ end }})"
}
use devbox
{{ if .EnvFile }}
Expand Down
10 changes: 10 additions & 0 deletions testscripts/generate/direnv-envrcdir-config-sibling.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also
# references a devbox config in another dir (./cfg) that is a sibling to the first.

mkdir cfg
exec devbox init cfg
exists cfg/devbox.json

mkdir dir
exec devbox generate direnv --envrc-dir dir --config cfg
grep 'eval "\$\(devbox generate direnv --print-envrc --config ../cfg\)"' dir/.envrc
9 changes: 9 additions & 0 deletions testscripts/generate/direnv-envrcdir-config-subdir.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also
# references a devbox config in another dir (./cfg) that is a subdir of the first.

mkdir dir/cfg
exec devbox init dir/cfg
exists dir/cfg/devbox.json

exec devbox generate direnv --envrc-dir dir --config dir/cfg
grep 'eval "\$\(devbox generate direnv --print-envrc --config cfg\)"' dir/.envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Testscript to validate generating a direnv .envrc in the current location that
# references a devbox config in a subdir (./cfg).

mkdir cfg
exec devbox init cfg
exists cfg/devbox.json

exec devbox generate direnv --envrc-dir . --config cfg
grep 'eval "\$\(devbox generate direnv --print-envrc --config cfg\)"' ./.envrc
9 changes: 9 additions & 0 deletions testscripts/generate/direnv-envrcdir.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Testscript to validate generating a direnv .envrc in a specified location (./cfg) that also
# references a devbox config in the same location (no --config needed)

mkdir cfg
exec devbox init cfg
exists cfg/devbox.json

exec devbox generate direnv --envrc-dir cfg
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' cfg/.envrc
11 changes: 11 additions & 0 deletions testscripts/generate/direnv-printenvrc-config.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Testscript to validate generating the contents of the .envrc file. In this case the
# config location is ignored because there is no --envrc-dir param.

exec devbox init
exec devbox generate direnv --print-envrc --config config-dir
cp stdout results.txt
grep 'watch_file config-dir/devbox.json config-dir/devbox.lock' results.txt
grep 'eval "\$\(devbox shellenv --init-hook --install --no-refresh-alias --config config-dir\)"' results.txt
! exists .envrc
! exists config-dir
! exists config-dir/.envrc
6 changes: 6 additions & 0 deletions testscripts/generate/direnv-printenvrc-envrcdir.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Testscript to validate that the --print-envrc and --envrc-dir params are not allowed
# to be used at the same time.

exec devbox init
! exec devbox generate direnv --print-envrc --envrc-dir dir
stderr 'Cannot use --print-envrc with --envrc-dir'
8 changes: 8 additions & 0 deletions testscripts/generate/direnv-printenvrc.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Testscript to validate the output of --print-envrc

exec devbox init
exec devbox generate direnv --print-envrc
cp stdout results.txt
grep 'watch_file devbox.json devbox.lock' results.txt
grep 'eval "\$\(devbox shellenv --init-hook --install --no-refresh-alias \)"' results.txt
! exists .envrc
5 changes: 4 additions & 1 deletion testscripts/generate/direnv.test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# Testscript to validate generating the contents of the .envrc file.

exec devbox init
exec devbox generate direnv
exists .envrc

exists devbox.json
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' .envrc