Skip to content
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, 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, err
}

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.jetify.com/docs/devbox/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