Skip to content

Commit 6c517a0

Browse files
Add --envrc-dir flag to allow specifying location of direnv config (#2629)
## Summary Fixes #2459 - devbox generate direnv does not respect --config It seems that the previous mode of operation for `devbox generate direnv --config <some-dir>` used `--config` to determine the location of the resulting `.envrc` file, rather than as the location of the `devbox.json` file. But in some cases it is desired to be able to specify not only the location of not only the direnv `.envrc` file, but also the devbox config file. Or at least to be able to specify just the location of the devbox config with the `.envrc` being in the current directory. To accomplish this, without breaking the current mode of operation, a new parameter is added, `--envrc-dir`, which specifies the location of the resulting `.envrc` file. For example, the command `devbox generate direnv --envrc-dir ABC --config ABC/XYZ` would create `ABC/.envrc` and within that would be the command `eval "$(devbox generate direnv --print-envrc --config XYZ)"` Without the new `--envrc-dir` param, operation is the same as it was previously. The output from `--print-envrc` now invokes `devbox shellenv...` prior to the `watch_file` command. This allows us to use `$DEVBOX_PROJECT_ROOT` to specify the path to the config and lock files. This ensures the correct files are being watched. ## How was it tested? Several new tests have been created to cover cases with and without the new `--envrc-dir` param. ## Community Contribution License All community contributions in this pull request are licensed to the project maintainers under the terms of the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). By creating this pull request, I represent that I have the right to license the contributions to the project maintainers under the Apache 2 License as stated in the [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). --------- Co-authored-by: Mike Landau <mikeland86@gmail.com>
1 parent f6994ef commit 6c517a0

22 files changed

+450
-32
lines changed

internal/boxcli/generate.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type generateCmdFlags struct {
2424
force bool
2525
printEnvrcContent bool
2626
rootUser bool
27+
envrcDir string // only used by generate direnv command
2728
}
2829

2930
type generateDockerfileCmdFlags struct {
@@ -147,10 +148,22 @@ func direnvCmd() *cobra.Command {
147148
command.Flags().BoolVarP(
148149
&flags.force, "force", "f", false, "force overwrite existing files")
149150
command.Flags().BoolVarP(
150-
&flags.printEnvrcContent, "print-envrc", "p", false, "output contents of devbox configuration to use in .envrc")
151+
&flags.printEnvrcContent, "print-envrc", "p", false,
152+
"output contents of devbox configuration to use in .envrc")
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 flag is useful for users who want to
160+
// keep their .envrc and devbox config files in different locations.
161+
command.Flags().StringVar(
162+
&flags.envrcDir, "envrc-dir", "",
163+
"path to directory where the .envrc file should be generated.\n"+
164+
"If not specified, the .envrc file will be generated in the same directory as\n"+
165+
"the devbox.json.")
166+
154167
flags.config.register(command)
155168
return command
156169
}
@@ -266,9 +279,17 @@ func runGenerateCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
266279
}
267280

268281
func runGenerateDirenvCmd(cmd *cobra.Command, flags *generateCmdFlags) error {
282+
// --print-envrc is used within the .envrc file and therefore doesn't make sense to also
283+
// use it with --envrc-dir, which specifies a directory where the .envrc file should be generated.
284+
if flags.printEnvrcContent && flags.envrcDir != "" {
285+
return usererr.New(
286+
"Cannot use --print-envrc with --envrc-dir. " +
287+
"Use --envrc-dir to specify the directory where the .envrc file should be generated.")
288+
}
289+
269290
if flags.printEnvrcContent {
270291
return devbox.PrintEnvrcContent(
271-
cmd.OutOrStdout(), devopt.EnvFlags(flags.envFlag))
292+
cmd.OutOrStdout(), devopt.EnvFlags(flags.envFlag), flags.config.path)
272293
}
273294

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

283-
return box.GenerateEnvrcFile(
284-
cmd.Context(), flags.force, devopt.EnvFlags(flags.envFlag))
304+
generateEnvrcOpts := devopt.EnvrcOpts{
305+
EnvFlags: devopt.EnvFlags(flags.envFlag),
306+
Force: flags.force,
307+
EnvrcDir: flags.envrcDir,
308+
ConfigDir: flags.config.path,
309+
}
310+
311+
return box.GenerateEnvrcFile(cmd.Context(), generateEnvrcOpts)
285312
}

internal/devbox/devbox.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -527,21 +527,28 @@ 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, envFlags devopt.EnvFlags, configDir string) error {
531+
return generate.EnvrcContent(w, envFlags, configDir)
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, opts devopt.EnvrcOpts) error {
536536
ctx, task := trace.NewTask(ctx, "devboxGenerateEnvrc")
537537
defer task.End()
538538

539-
envrcfilePath := filepath.Join(d.projectDir, ".envrc")
540-
filesExist := fileutil.Exists(envrcfilePath)
541-
if !force && filesExist {
539+
// If no envrcDir was specified, use the configDir. This is for backward compatibility
540+
// where the .envrc was placed in the same location as specified by --config. Note that
541+
// if that is also blank, the .envrc will be generated in the current working directory.
542+
if opts.EnvrcDir == "" {
543+
opts.EnvrcDir = opts.ConfigDir
544+
}
545+
546+
envrcFilePath := filepath.Join(opts.EnvrcDir, ".envrc")
547+
filesExist := fileutil.Exists(envrcFilePath)
548+
if !opts.Force && filesExist {
542549
return usererr.New(
543-
"A .envrc is already present in the current directory. " +
544-
"Remove it or use --force to overwrite it.",
550+
"A .envrc is already present in %q. Remove it or use --force to overwrite it.",
551+
opts.EnvrcDir,
545552
)
546553
}
547554

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

553560
// .envrc file creation
554-
err := generate.CreateEnvrc(ctx, d.projectDir, envFlags)
561+
err := generate.CreateEnvrc(ctx, opts)
555562
if err != nil {
556563
return errors.WithStack(err)
557564
}
558-
ux.Fsuccessf(d.stderr, "generated .envrc file\n")
565+
ux.Fsuccessf(d.stderr, "generated .envrc file in %q.\n", opts.EnvrcDir)
559566
if cmdutil.Exists("direnv") {
560-
cmd := exec.Command("direnv", "allow")
567+
cmd := exec.Command("direnv", "allow", opts.EnvrcDir)
561568
err := cmd.Run()
562569
if err != nil {
563570
return errors.WithStack(err)
564571
}
565-
ux.Fsuccessf(d.stderr, "ran `direnv allow`\n")
572+
ux.Fsuccessf(d.stderr, "ran `direnv allow %s`\n", opts.EnvrcDir)
566573
}
567574
return nil
568575
}

internal/devbox/devopt/devboxopts.go

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

37+
type EnvrcOpts struct {
38+
EnvFlags
39+
Force bool
40+
EnvrcDir string
41+
ConfigDir string
42+
}
43+
3744
type PullboxOpts struct {
3845
Overwrite bool
3946
URL string

internal/devbox/generate/devcontainer_util.go

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,35 +140,68 @@ 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.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.EnvFile))
162+
}
163+
164+
configDir, err := getRelativePathToConfig(opts.EnvrcDir, opts.ConfigDir)
165+
if err != nil {
166+
return err
162167
}
163168

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

166171
// write content into file
167172
return t.Execute(file, map[string]string{
168-
"Flags": strings.Join(flags, " "),
173+
"EnvFlag": strings.Join(flags, " "),
174+
"ConfigDir": formatConfigDirArg(configDir),
169175
})
170176
}
171177

178+
// Returns the relative path from sourceDir to configDir, or an error if it cannot be determined.
179+
func getRelativePathToConfig(sourceDir, configDir string) (string, error) {
180+
absConfigDir, err := filepath.Abs(configDir)
181+
if err != nil {
182+
return "", fmt.Errorf("failed to get absolute path for config dir: %w", err)
183+
}
184+
185+
absSourceDir, err := filepath.Abs(sourceDir)
186+
if err != nil {
187+
return "", fmt.Errorf("failed to get absolute path for source dir: %w", err)
188+
}
189+
190+
// We don't want the path if the config dir is a parent of the envrc dir. This way
191+
// the config will be found when it recursively searches for it through the parent tree.
192+
if strings.HasPrefix(absSourceDir, absConfigDir) {
193+
return "", nil
194+
}
195+
196+
relPath, err := filepath.Rel(absSourceDir, absConfigDir)
197+
if err != nil {
198+
// If a relative path cannot be computed, return the absolute path of configDir
199+
return absConfigDir, err
200+
}
201+
202+
return relPath, nil
203+
}
204+
172205
func (g *Options) getDevcontainerContent() *devcontainerObject {
173206
// object that gets written in devcontainer.json
174207
devcontainerContent := &devcontainerObject{
@@ -219,17 +252,26 @@ func (g *Options) getDevcontainerContent() *devcontainerObject {
219252
return devcontainerContent
220253
}
221254

222-
func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags) error {
223-
tmplName := "envrcContent.tmpl"
224-
t := template.Must(template.ParseFS(tmplFS, "tmpl/"+tmplName))
255+
func EnvrcContent(w io.Writer, envFlags devopt.EnvFlags, configDir string) error {
256+
t := template.Must(template.ParseFS(tmplFS, "tmpl/envrcContent.tmpl"))
225257
envFlag := ""
226258
if len(envFlags.EnvMap) > 0 {
227259
for k, v := range envFlags.EnvMap {
228260
envFlag += fmt.Sprintf("--env %s=%s ", k, v)
229261
}
230262
}
263+
231264
return t.Execute(w, map[string]string{
232-
"EnvFlag": envFlag,
233-
"EnvFile": envFlags.EnvFile,
265+
"EnvFlag": envFlag,
266+
"EnvFile": envFlags.EnvFile,
267+
"ConfigDir": formatConfigDirArg(configDir),
234268
})
235269
}
270+
271+
func formatConfigDirArg(configDir string) string {
272+
if configDir == "" {
273+
return ""
274+
}
275+
276+
return "--config " + configDir
277+
}

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 .EnvFlag}} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})"
77
88
# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
99
# for more details

internal/devbox/generate/tmpl/envrcContent.tmpl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use_devbox() {
2-
watch_file devbox.json devbox.lock
3-
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }})"
2+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias{{ if .EnvFlag }} {{ .EnvFlag }}{{ end }}{{ if .ConfigDir }} {{ .ConfigDir }}{{ end }})"
3+
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
44
}
55
use devbox
66
{{ if .EnvFile }}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Testscript to validate generating the contents of the .envrc file.
2+
# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and
3+
# the config will be found there, which means the `--print-env` doesn't need to specify the dir.
4+
# This matches the mode of operation prior to the addition of the --envrc-dir flag.
5+
6+
mkdir dir
7+
exec devbox init dir
8+
exists dir/devbox.json
9+
10+
exec devbox generate direnv --env x=y --config dir
11+
grep 'eval "\$\(devbox generate direnv --print-envrc --env x=y\)"' dir/.envrc
12+
13+
cd dir
14+
exec devbox generate direnv --print-envrc --env x=y
15+
16+
cmp stdout ../expected-results.txt
17+
18+
# Note: The contents of the following file ends with two blank lines. This is
19+
# necessary to match the blank line that follows "use devbox" in the actual output.
20+
-- expected-results.txt --
21+
use_devbox() {
22+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )"
23+
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
24+
}
25+
use devbox
26+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Testscript to validate generating the contents of the .envrc file.
2+
# Note that since --envrc-dir was NOT specified, the .envrc will be in the `dir` directory and
3+
# the config will be found there, which means the `--print-env` doesn't need to specify the dir.
4+
# This matches the mode of operation prior to the addition of the --envrc-dir flag.
5+
6+
mkdir dir
7+
exec devbox init dir
8+
exists dir/devbox.json
9+
10+
exec devbox generate direnv --config dir
11+
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' dir/.envrc
12+
13+
cd dir
14+
exec devbox generate direnv --print-envrc
15+
16+
cmp stdout ../expected-results.txt
17+
18+
# Note: The contents of the following file ends with two blank lines. This is
19+
# necessary to match the blank line that follows "use devbox" in the actual output.
20+
-- expected-results.txt --
21+
use_devbox() {
22+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias)"
23+
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
24+
}
25+
use devbox
26+
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Testscript to validate generating the contents of the .envrc file.
2+
3+
exec devbox init
4+
exists devbox.json
5+
6+
exec devbox generate direnv --env x=y
7+
grep 'eval "\$\(devbox generate direnv --print-envrc --env x=y\)"' .envrc
8+
9+
exec devbox generate direnv --print-envrc --env x=y
10+
11+
cmp stdout expected-results.txt
12+
13+
# Note: The contents of the following file ends with two blank lines. This is
14+
# necessary to match the blank line that follows "use devbox" in the actual output.
15+
-- expected-results.txt --
16+
use_devbox() {
17+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias --env x=y )"
18+
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
19+
}
20+
use devbox
21+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Testscript to validate generating a direnv .envrc in a specified location (./dir).
2+
# The devbox config is in the current dir (parent to ./dir). Since no --config
3+
# is specified, the normal config-finding will find the config.
4+
5+
exec devbox init
6+
exists ./devbox.json
7+
8+
mkdir dir
9+
10+
exec devbox generate direnv --envrc-dir dir
11+
grep 'eval "\$\(devbox generate direnv --print-envrc\)"' dir/.envrc
12+
! grep '--config' dir/.envrc # redundant, but making expectations obvious
13+
14+
cd dir
15+
exec devbox generate direnv --print-envrc
16+
17+
cmp stdout ../expected-results.txt
18+
19+
# Note: The contents of the following file ends with two blank lines. This is
20+
# necessary to match the blank line that follows "use devbox" in the actual output.
21+
-- expected-results.txt --
22+
use_devbox() {
23+
eval "$(devbox shellenv --init-hook --install --no-refresh-alias)"
24+
watch_file $DEVBOX_PROJECT_ROOT/devbox.json $DEVBOX_PROJECT_ROOT/devbox.lock
25+
}
26+
use devbox
27+

0 commit comments

Comments
 (0)