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 6 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
35 changes: 31 additions & 4 deletions internal/boxcli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 @@ -147,10 +148,22 @@ func direnvCmd() *cobra.Command {
command.Flags().BoolVarP(
&flags.force, "force", "f", false, "force overwrite existing files")
command.Flags().BoolVarP(
&flags.printEnvrcContent, "print-envrc", "p", false, "output contents of devbox configuration to use in .envrc")
&flags.printEnvrcContent, "print-envrc", "p", false,
"output contents of devbox configuration to use in .envrc")
// 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 flag 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.\n"+
"If not specified, the .envrc file will be generated in the same directory as\n"+
"the devbox.json.")

flags.config.register(command)
return command
}
Expand Down Expand Up @@ -266,9 +279,17 @@ 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.")
}

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

box, err := devbox.Open(&devopt.Opts{
Expand All @@ -280,6 +301,12 @@ func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
return errors.WithStack(err)
}

return box.GenerateEnvrcFile(
cmd.Context(), flags.force, devopt.EnvFlags(flags.envFlag))
generateEnvrcOpts := devopt.EnvrcOpts{
EnvFlags: devopt.EnvFlags(flags.envFlag),
Force: flags.force,
EnvrcDir: flags.envrcDir,
ConfigDir: flags.config.path,
}

return box.GenerateEnvrcFile(cmd.Context(), generateEnvrcOpts)
}
31 changes: 19 additions & 12 deletions internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -527,21 +527,28 @@ 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, envFlags devopt.EnvFlags, configDir string) error {
return generate.EnvrcContent(w, envFlags, configDir)
}

// 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, opts devopt.EnvrcOpts) error {
ctx, task := trace.NewTask(ctx, "devboxGenerateEnvrc")
defer task.End()

envrcfilePath := filepath.Join(d.projectDir, ".envrc")
filesExist := fileutil.Exists(envrcfilePath)
if !force && filesExist {
// If no envrcDir was specified, use the configDir. This is for backward compatibility
// where the .envrc was placed in the same location as specified by --config. Note that
// if that is also blank, the .envrc will be generated in the current working directory.
if opts.EnvrcDir == "" {
opts.EnvrcDir = opts.ConfigDir
}

envrcFilePath := filepath.Join(opts.EnvrcDir, ".envrc")
filesExist := fileutil.Exists(envrcFilePath)
if !opts.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 +558,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
7 changes: 7 additions & 0 deletions internal/devbox/devopt/devboxopts.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ type EnvFlags struct {
EnvFile string
}

type EnvrcOpts struct {
EnvFlags
Force bool
EnvrcDir string
ConfigDir string
}

type PullboxOpts struct {
Overwrite bool
URL string
Expand Down
66 changes: 54 additions & 12 deletions internal/devbox/generate/devcontainer_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,35 +140,68 @@ 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))
}

configDir, err := getRelativePathToConfig(opts.EnvrcDir, opts.ConfigDir)
if err != nil {
return err
}

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

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

// Returns the relative path from sourceDir to configDir, or an error if it cannot be determined.
func getRelativePathToConfig(sourceDir string, configDir string) (string, error) {
absConfigDir, err := filepath.Abs(configDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for config dir: %w", err)
}

absSourceDir, err := filepath.Abs(sourceDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for source dir: %w", err)
}

// We don't want the path if the config dir is a parent of the envrc dir. This way
// the config will be found when it recursively searches for it through the parent tree.
if strings.HasPrefix(absSourceDir, absConfigDir) {
return "", nil
}

relPath, err := filepath.Rel(absSourceDir, absConfigDir)
if err != nil {
// If a relative path cannot be computed, return the absolute path of configDir
return absConfigDir, nil
}

return relPath, nil
}

func (g *Options) getDevcontainerContent() *devcontainerObject {
// object that gets written in devcontainer.json
devcontainerContent := &devcontainerObject{
Expand Down Expand Up @@ -219,17 +252,26 @@ func (g *Options) getDevcontainerContent() *devcontainerObject {
return devcontainerContent
}

func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
tmplName := "envrcContent.tmpl"
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags, configDir string) error {
t := template.Must(template.ParseFS(tmplFS, "tmpl/envrcContent.tmpl"))
envFlag := ""
if len(envFlags.EnvMap) > 0 {
for k, v := range envFlags.EnvMap {
envFlag += fmt.Sprintf("--env %s=%s ", k, v)
}
}

return t.Execute(w, map[string]string{
"EnvFlag": envFlag,
"EnvFile": envFlags.EnvFile,
"EnvFlag": envFlag,
"EnvFile": envFlags.EnvFile,
"ConfigDir": formatConfigDirArg(configDir),
})
}

func formatConfigDirArg(configDir string) string {
if configDir == "" {
return ""
}

return "--config " + configDir
}
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 .EnvFlag}} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})"

# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
# for more details
4 changes: 2 additions & 2 deletions internal/devbox/generate/tmpl/envrcContent.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use_devbox() {
watch_file devbox.json devbox.lock
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }})"
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox
{{ if .EnvFile }}
Expand Down
26 changes: 26 additions & 0 deletions testscripts/generate/direnv-config-envflag.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Testscript to validate generating the contents of the .envrc file.
# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and
# the config will be found there, which means the `--print-env` doesn't need to specify the dir.
# This matches the mode of operation prior to the addition of the --envrc-dir flag.

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

exec devbox generate direnv --env x=y --config dir
grep 'eval "\$\(devbox generate direnv --print-envrc --env x=y\)"' dir/.envrc

cd dir
exec devbox generate direnv --print-envrc --env x=y

cmp stdout ../expected-results.txt

# Note: The contents of the following file ends with two blank lines. This is
# necessary to match the blank line that follows "use devbox" in the actual output.
-- expected-results.txt --
use_devbox() {
eval "$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox

26 changes: 26 additions & 0 deletions testscripts/generate/direnv-config.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Testscript to validate generating the contents of the .envrc file.
# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and
# the config will be found there, which means the `--print-env` doesn't need to specify the dir.
# This matches the mode of operation prior to the addition of the --envrc-dir flag.

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

exec devbox generate direnv --config dir
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' dir/.envrc

cd dir
exec devbox generate direnv --print-envrc

cmp stdout ../expected-results.txt

# Note: The contents of the following file ends with two blank lines. This is
# necessary to match the blank line that follows "use devbox" in the actual output.
-- expected-results.txt --
use_devbox() {
eval "$(devbox shellenv --init-hook --install --no-refresh-alias)"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox

21 changes: 21 additions & 0 deletions testscripts/generate/direnv-envflag.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Testscript to validate generating the contents of the .envrc file.

exec devbox init
exists devbox.json

exec devbox generate direnv --env x=y
grep 'eval "\$\(devbox generate direnv --print-envrc --env x=y\)"' .envrc

exec devbox generate direnv --print-envrc --env x=y

cmp stdout expected-results.txt

# Note: The contents of the following file ends with two blank lines. This is
# necessary to match the blank line that follows "use devbox" in the actual output.
-- expected-results.txt --
use_devbox() {
eval "$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox

27 changes: 27 additions & 0 deletions testscripts/generate/direnv-envrcdir-config-parent.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Testscript to validate generating a direnv .envrc in a specified location (./dir).
# The devbox config is in the current dir (parent to ./dir). Since no --config
# is specified, the normal config-finding will find the config.

exec devbox init
exists ./devbox.json

mkdir dir

exec devbox generate direnv --envrc-dir dir
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' dir/.envrc
! grep '--config' dir/.envrc # redundant, but making expectations obvious

cd dir
exec devbox generate direnv --print-envrc

cmp stdout ../expected-results.txt

# Note: The contents of the following file ends with two blank lines. This is
# necessary to match the blank line that follows "use devbox" in the actual output.
-- expected-results.txt --
use_devbox() {
eval "$(devbox shellenv --init-hook --install --no-refresh-alias)"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox

25 changes: 25 additions & 0 deletions testscripts/generate/direnv-envrcdir-config-sibling.test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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

cd dir
exec devbox generate direnv --print-envrc --config ../cfg

cmp stdout ../expected-results.txt

# Note: The contents of the following file ends with two blank lines. This is
# necessary to match the blank line that follows "use devbox" in the actual output.
-- expected-results.txt --
use_devbox() {
eval "$(devbox shellenv --init-hook --install --no-refresh-alias --config ../cfg)"
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
}
use devbox

Loading