Skip to content

Commit 415686e

Browse files
Add --envrc-dir flag to allow specifying location of direnv config
1 parent dcf8225 commit 415686e

14 files changed

+155
-23
lines changed

internal/boxcli/generate.go

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package boxcli
66
import (
77
"cmp"
88
"fmt"
9+
"path/filepath"
910
"regexp"
1011

1112
"github.com/pkg/errors"
@@ -24,6 +25,7 @@ type generateCmdFlags struct {
2425
force bool
2526
printEnvrcContent bool
2627
rootUser bool
28+
envrcDir string // only used by generate direnv command
2729
}
2830

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

156+
// --envrc-dir allows users to specify a directory where the .envrc file should be generated
157+
// separately from the devbox config directory. Without this flag, the .envrc file
158+
// will be generated in the same directory as the devbox config file (i.e., either the current
159+
// directory or the directory specified by --config). This is useful for users who want to keep
160+
// their .envrc and devbox config files in different locations.
161+
command.Flags().StringVar(
162+
&flags.envrcDir, "envrc-dir", "", "path to directory where the .envrc file should be generated. "+
163+
"If not specified, the .envrc file will be generated in the current working directory.")
164+
154165
flags.config.register(command)
155166
return command
156167
}
@@ -266,13 +277,32 @@ func runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
266277
}
267278

268279
func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
280+
// --print-envrc is used within the .envrc file and therefore doesn't make sense to also
281+
// use it with --envrc-dir, which specifies a directory where the .envrc file should be generated.
282+
if flags.printEnvrcContent && flags.envrcDir != "" {
283+
return usererr.New(
284+
"Cannot use --print-envrc with --envrc-dir. " +
285+
"Use --envrc-dir to specify the directory where the .envrc file should be generated.")
286+
}
287+
288+
// Determine the directories for .envrc and config
289+
configDir, envrcDir, err := determineDirenvDirs(flags.config.path, flags.envrcDir)
290+
if err != nil {
291+
return errors.WithStack(err)
292+
}
293+
294+
generateOpts := devopt.EnvrcOpts{
295+
EnvrcDir: envrcDir,
296+
ConfigDir: configDir,
297+
EnvFlags: devopt.EnvFlags(flags.envFlag),
298+
}
299+
269300
if flags.printEnvrcContent {
270-
return devbox.PrintEnvrcContent(
271-
cmd.OutOrStdout(), devopt.EnvFlags(flags.envFlag))
301+
return devbox.PrintEnvrcContent(cmd.OutOrStdout(), generateOpts)
272302
}
273303

274304
box, err := devbox.Open(&devopt.Opts{
275-
Dir: flags.config.path,
305+
Dir: filepath.Join(envrcDir, configDir),
276306
Environment: flags.config.environment,
277307
Stderr: cmd.ErrOrStderr(),
278308
})
@@ -281,5 +311,33 @@ func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
281311
}
282312

283313
return box.GenerateEnvrcFile(
284-
cmd.Context(), flags.force, devopt.EnvFlags(flags.envFlag))
314+
cmd.Context(), flags.force, generateOpts)
315+
}
316+
317+
// Returns cononical paths for configDir and envrcDir. Both locations are relative to the current
318+
// working directory when provided to this function. However, since the config file will ultimately
319+
// be relative to the .envrc file, we need to determine the relative path from envrcDir to configDir.
320+
func determineDirenvDirs(configDir, envrcDir string) (string, string, error) {
321+
// If envrcDir is specified, use it as the directory for .envrc and
322+
// then determine configDir relative to that.
323+
if envrcDir != "" {
324+
if configDir == "" {
325+
return "", envrcDir, nil
326+
}
327+
328+
relativeConfigDir, err := filepath.Rel(envrcDir, configDir)
329+
if err != nil {
330+
return "", "", errors.Wrapf(err, "failed to determine relative path from %s to %s", envrcDir, configDir)
331+
}
332+
333+
// If the relative path is ".", it means configDir is the same as envrcDir.
334+
if relativeConfigDir == "." {
335+
relativeConfigDir = ""
336+
}
337+
338+
return relativeConfigDir, envrcDir, nil
339+
}
340+
// If envrcDir is not specified, we will use the configDir as the location for .envrc. This is
341+
// for backward compatibility (prior to the --envrc-dir flag being introduced).
342+
return "", configDir, nil
285343
}

internal/devbox/devbox.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -527,21 +527,21 @@ func (d *Devbox) GenerateDockerfile(ctx context.Context, generateOpts devopt.Gen
527527
}))
528528
}
529529

530-
func PrintEnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
531-
return generate.EnvrcContent(w, envFlags)
530+
func PrintEnvrcContent(w io.Writer, opts devopt.EnvrcOpts) error {
531+
return generate.EnvrcContent(w, opts)
532532
}
533533

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

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

@@ -551,11 +551,11 @@ func (d *Devbox) GenerateEnvrcFile(ctx context.Context, force bool, envFlags dev
551551
}
552552

553553
// .envrc file creation
554-
err := generate.CreateEnvrc(ctx, d.projectDir, envFlags)
554+
err := generate.CreateEnvrc(ctx, opts)
555555
if err != nil {
556556
return errors.WithStack(err)
557557
}
558-
ux.Fsuccessf(d.stderr, "generated .envrc file\n")
558+
ux.Fsuccessf(d.stderr, "generated .envrc file in %q.\n", opts.EnvrcDir)
559559
if cmdutil.Exists("direnv") {
560560
cmd := exec.Command("direnv", "allow")
561561
err := cmd.Run()

internal/devbox/devopt/devboxopts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ type EnvFlags struct {
3434
EnvFile string
3535
}
3636

37+
type EnvrcOpts struct {
38+
EnvFlags
39+
EnvrcDir string
40+
ConfigDir string
41+
}
42+
3743
type PullboxOpts struct {
3844
Overwrite bool
3945
URL string

internal/devbox/generate/devcontainer_util.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,32 +140,33 @@ func (g *Options) CreateDevcontainer(ctx context.Context) error {
140140
return err
141141
}
142142

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

146146
// create .envrc file
147-
file, err := os.Create(filepath.Join(path, ".envrc"))
147+
file, err := os.Create(filepath.Join(opts.EnvrcDir, ".envrc"))
148148
if err != nil {
149149
return err
150150
}
151151
defer file.Close()
152152

153153
flags := []string{}
154154

155-
if len(envFlags.EnvMap) > 0 {
156-
for k, v := range envFlags.EnvMap {
155+
if len(opts.EnvMap) > 0 {
156+
for k, v := range opts.EnvFlags.EnvMap {
157157
flags = append(flags, fmt.Sprintf("--env %s=%s", k, v))
158158
}
159159
}
160-
if envFlags.EnvFile != "" {
161-
flags = append(flags, fmt.Sprintf("--env-file %s", envFlags.EnvFile))
160+
if opts.EnvFile != "" {
161+
flags = append(flags, fmt.Sprintf("--env-file %s", opts.EnvFlags.EnvFile))
162162
}
163163

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

166166
// write content into file
167167
return t.Execute(file, map[string]string{
168168
"Flags": strings.Join(flags, " "),
169+
"Dir": opts.ConfigDir,
169170
})
170171
}
171172

@@ -219,7 +220,7 @@ func (g *Options) getDevcontainerContent() *devcontainerObject {
219220
return devcontainerContent
220221
}
221222

222-
func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
223+
func EnvrcContent(w io.Writer, envFlags devopt.EnvrcOpts) error {
223224
tmplName := "envrcContent.tmpl"
224225
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
225226
envFlag := ""
@@ -231,5 +232,6 @@ func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
231232
return t.Execute(w, map[string]string{
232233
"EnvFlag": envFlag,
233234
"EnvFile": envFlags.EnvFile,
235+
"Dir": envFlags.EnvrcDir,
234236
})
235237
}

internal/devbox/generate/tmpl/envrc.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Automatically sets up your devbox environment whenever you cd into this
44
# directory via our direnv integration:
55

6-
eval "$(devbox generate direnv --print-envrc{{ if .Flags}} {{ .Flags }}{{ end }})"
6+
eval "$(devbox generate direnv --print-envrc{{ if .Flags}} {{ .Flags }}{{ end }}{{ if .Dir }} --config {{ .Dir -}}{{ end }})"
77
88
# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/
99
# for more details

internal/devbox/generate/tmpl/envrcContent.tmpl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
{{define "DirPrefix"}}{{ if .Dir }}{{ .Dir }}/{{ end }}{{end}}
12
use_devbox() {
2-
watch_file devbox.json devbox.lock
3-
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }})"
3+
watch_file {{template "DirPrefix" .}}devbox.json {{template "DirPrefix" .}}devbox.lock
4+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias {{ if .EnvFlag }}{{ .EnvFlag }}{{ end }} {{- if .Dir }}--config {{ .Dir -}}{{ end }})"
45
}
56
use devbox
67
{{ if .EnvFile }}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also
2+
# references a devbox config in another dir (./cfg) that is a sibling to the first.
3+
4+
mkdir cfg
5+
exec devbox init cfg
6+
exists cfg/devbox.json
7+
8+
mkdir dir
9+
exec devbox generate direnv --envrc-dir dir --config cfg
10+
grep 'eval "\$\(devbox generate direnv --print-envrc --config ../cfg\)"' dir/.envrc
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Testscript to validate generating a direnv .envrc in a specified location (./dir) that also
2+
# references a devbox config in another dir (./cfg) that is a subdir of the first.
3+
4+
mkdir dir/cfg
5+
exec devbox init dir/cfg
6+
exists dir/cfg/devbox.json
7+
8+
exec devbox generate direnv --envrc-dir dir --config dir/cfg
9+
grep 'eval "\$\(devbox generate direnv --print-envrc --config cfg\)"' dir/.envrc
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Testscript to validate generating a direnv .envrc in the current location that
2+
# references a devbox config in a subdir (./cfg).
3+
4+
mkdir cfg
5+
exec devbox init cfg
6+
exists cfg/devbox.json
7+
8+
exec devbox generate direnv --envrc-dir . --config cfg
9+
grep 'eval "\$\(devbox generate direnv --print-envrc --config cfg\)"' ./.envrc
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Testscript to validate generating a direnv .envrc in a specified location (./cfg) that also
2+
# references a devbox config in the same location (no --config needed)
3+
4+
mkdir cfg
5+
exec devbox init cfg
6+
exists cfg/devbox.json
7+
8+
exec devbox generate direnv --envrc-dir cfg
9+
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' cfg/.envrc

0 commit comments

Comments
 (0)