diff --git a/internal/boxcli/run.go b/internal/boxcli/run.go index ac4f832b2af..1506fc94ccb 100644 --- a/internal/boxcli/run.go +++ b/internal/boxcli/run.go @@ -17,14 +17,16 @@ import ( "go.jetpack.io/devbox/internal/devbox" "go.jetpack.io/devbox/internal/devbox/devopt" "go.jetpack.io/devbox/internal/redact" + "go.jetpack.io/devbox/internal/ux" ) type runCmdFlags struct { envFlag - config configFlags - omitNixEnv bool - pure bool - listScripts bool + config configFlags + omitNixEnv bool + pure bool + listScripts bool + recomputeEnv bool } // runFlagDefaults are the flag default values that differ @@ -62,6 +64,7 @@ func runCmd(defaults runFlagDefaults) *cobra.Command { "shell environment will omit the env-vars from print-dev-env", ) _ = command.Flags().MarkHidden("omit-nix-env") + command.Flags().BoolVar(&flags.recomputeEnv, "recompute", true, "recompute environment if needed") command.ValidArgs = listScripts(command, flags) @@ -84,6 +87,7 @@ func listScripts(cmd *cobra.Command, flags runCmdFlags) []string { } func runScriptCmd(cmd *cobra.Command, args []string, flags runCmdFlags) error { + ctx := cmd.Context() if len(args) == 0 || flags.listScripts { scripts := listScripts(cmd, flags) if len(scripts) == 0 { @@ -111,19 +115,32 @@ func runScriptCmd(cmd *cobra.Command, args []string, flags runCmdFlags) error { // Check the directory exists. box, err := devbox.Open(&devopt.Opts{ Dir: path, + Env: env, Environment: flags.config.environment, Stderr: cmd.ErrOrStderr(), - Env: env, }) if err != nil { return redact.Errorf("error reading devbox.json: %w", err) } envOpts := devopt.EnvOptions{ - OmitNixEnv: flags.omitNixEnv, - Pure: flags.pure, + Hooks: devopt.LifecycleHooks{ + OnStaleState: func() { + if !flags.recomputeEnv { + ux.FHidableWarning( + ctx, + cmd.ErrOrStderr(), + devbox.StateOutOfDateMessage, + "with --recompute=true", + ) + } + }, + }, + OmitNixEnv: flags.omitNixEnv, + Pure: flags.pure, + SkipRecompute: !flags.recomputeEnv, } - if err := box.RunScript(cmd.Context(), envOpts, script, scriptArgs); err != nil { + if err := box.RunScript(ctx, envOpts, script, scriptArgs); err != nil { return redact.Errorf("error running script %q in Devbox: %w", script, err) } return nil diff --git a/internal/boxcli/shell.go b/internal/boxcli/shell.go index 48e0df4ed1f..a9f9009691d 100644 --- a/internal/boxcli/shell.go +++ b/internal/boxcli/shell.go @@ -13,14 +13,16 @@ import ( "go.jetpack.io/devbox/internal/devbox" "go.jetpack.io/devbox/internal/devbox/devopt" "go.jetpack.io/devbox/internal/envir" + "go.jetpack.io/devbox/internal/ux" ) type shellCmdFlags struct { envFlag - config configFlags - omitNixEnv bool - printEnv bool - pure bool + config configFlags + omitNixEnv bool + printEnv bool + pure bool + recomputeEnv bool } // shellFlagDefaults are the flag default values that differ @@ -53,6 +55,7 @@ func shellCmd(defaults shellFlagDefaults) *cobra.Command { "shell environment will omit the env-vars from print-dev-env", ) _ = command.Flags().MarkHidden("omit-nix-env") + command.Flags().BoolVar(&flags.recomputeEnv, "recompute", true, "recompute environment if needed") flags.config.register(command) flags.envFlag.register(command) @@ -60,10 +63,12 @@ func shellCmd(defaults shellFlagDefaults) *cobra.Command { } func runShellCmd(cmd *cobra.Command, flags shellCmdFlags) error { + ctx := cmd.Context() env, err := flags.Env(flags.config.path) if err != nil { return err } + // Check the directory exists. box, err := devbox.Open(&devopt.Opts{ Dir: flags.config.path, @@ -91,9 +96,22 @@ func runShellCmd(cmd *cobra.Command, flags shellCmdFlags) error { return shellInceptionErrorMsg("devbox shell") } - return box.Shell(cmd.Context(), devopt.EnvOptions{ - OmitNixEnv: flags.omitNixEnv, - Pure: flags.pure, + return box.Shell(ctx, devopt.EnvOptions{ + Hooks: devopt.LifecycleHooks{ + OnStaleState: func() { + if !flags.recomputeEnv { + ux.FHidableWarning( + ctx, + cmd.ErrOrStderr(), + devbox.StateOutOfDateMessage, + "with --recompute=true", + ) + } + }, + }, + OmitNixEnv: flags.omitNixEnv, + Pure: flags.pure, + SkipRecompute: !flags.recomputeEnv, }) } diff --git a/internal/boxcli/shellenv.go b/internal/boxcli/shellenv.go index a37472b7167..76d7e68ce21 100644 --- a/internal/boxcli/shellenv.go +++ b/internal/boxcli/shellenv.go @@ -116,11 +116,23 @@ func shellEnvFunc( } envStr, err := box.EnvExports(ctx, devopt.EnvExportsOpts{ - DontRecomputeEnvironment: !flags.recomputeEnv, EnvOptions: devopt.EnvOptions{ + Hooks: devopt.LifecycleHooks{ + OnStaleState: func() { + if !flags.recomputeEnv { + ux.FHidableWarning( + ctx, + cmd.ErrOrStderr(), + devbox.StateOutOfDateMessage, + box.RefreshAliasOrCommand(), + ) + } + }, + }, OmitNixEnv: flags.omitNixEnv, PreservePathStack: flags.preservePathStack, Pure: flags.pure, + SkipRecompute: !flags.recomputeEnv, }, NoRefreshAlias: flags.noRefreshAlias, RunHooks: flags.runInitHook, diff --git a/internal/devbox/devbox.go b/internal/devbox/devbox.go index dc571b431c6..fdc25741b08 100644 --- a/internal/devbox/devbox.go +++ b/internal/devbox/devbox.go @@ -351,22 +351,7 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st var envs map[string]string var err error - if opts.DontRecomputeEnvironment { - upToDate, _ := d.lockfile.IsUpToDateAndInstalled(isFishShell()) - if !upToDate { - ux.FHidableWarning( - ctx, - d.stderr, - StateOutOfDateMessage, - d.refreshAliasOrCommand(), - ) - } - - envs, err = d.computeEnv(ctx, true /*usePrintDevEnvCache*/, opts.EnvOptions) - } else { - envs, err = d.ensureStateIsUpToDateAndComputeEnv(ctx, opts.EnvOptions) - } - + envs, err = d.ensureStateIsUpToDateAndComputeEnv(ctx, opts.EnvOptions) if err != nil { return "", err } @@ -819,23 +804,35 @@ func (d *Devbox) ensureStateIsUpToDateAndComputeEnv( ) (map[string]string, error) { defer debug.FunctionTimer().End() - // When ensureStateIsUpToDate is called with ensure=true, it always - // returns early if the lockfile is up to date. So we don't need to check here - if err := d.ensureStateIsUpToDate(ctx, ensure); isConnectionError(err) { - if !fileutil.Exists(d.nixPrintDevEnvCachePath()) { - ux.Ferrorf( + upToDate, err := d.lockfile.IsUpToDateAndInstalled(isFishShell()) + if err != nil { + return nil, err + } + if !upToDate { + if envOpts.Hooks.OnStaleState != nil { + envOpts.Hooks.OnStaleState() + } + } + + if !envOpts.SkipRecompute { + // When ensureStateIsUpToDate is called with ensure=true, it always + // returns early if the lockfile is up to date. So we don't need to check here + if err := d.ensureStateIsUpToDate(ctx, ensure); isConnectionError(err) { + if !fileutil.Exists(d.nixPrintDevEnvCachePath()) { + ux.Ferrorf( + d.stderr, + "Error connecting to the internet and no cached environment found. Aborting.\n", + ) + return nil, err + } + ux.Fwarningf( d.stderr, - "Error connecting to the internet and no cached environment found. Aborting.\n", + "Error connecting to the internet. Will attempt to use cached environment.\n", ) + } else if err != nil { + // Some other non connection error, just return it. return nil, err } - ux.Fwarningf( - d.stderr, - "Error connecting to the internet. Will attempt to use cached environment.\n", - ) - } else if err != nil { - // Some other non connection error, just return it. - return nil, err } // Since ensureStateIsUpToDate calls computeEnv when not up do date, diff --git a/internal/devbox/devopt/devboxopts.go b/internal/devbox/devopt/devboxopts.go index df47b1c2174..1cca0dffb85 100644 --- a/internal/devbox/devopt/devboxopts.go +++ b/internal/devbox/devopt/devboxopts.go @@ -62,10 +62,9 @@ type UpdateOpts struct { } type EnvExportsOpts struct { - DontRecomputeEnvironment bool - EnvOptions EnvOptions - NoRefreshAlias bool - RunHooks bool + EnvOptions EnvOptions + NoRefreshAlias bool + RunHooks bool } // EnvOptions configure the Devbox Environment in the `computeEnv` function. @@ -73,7 +72,14 @@ type EnvExportsOpts struct { // like `shellenv`, `shell` and `run`. // - The struct is designed for the "common case" to be zero-initialized as `EnvOptions{}`. type EnvOptions struct { + Hooks LifecycleHooks OmitNixEnv bool PreservePathStack bool Pure bool + SkipRecompute bool +} + +type LifecycleHooks struct { + // OnStaleState is called when the Devbox state is out of date, AND it is not being recomputed. + OnStaleState func() } diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index eb631e26211..8ed878e34fc 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -303,7 +303,7 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er ctx, d.stderr, StateOutOfDateMessage, - d.refreshAliasOrCommand(), + d.RefreshAliasOrCommand(), ) } diff --git a/internal/devbox/refresh.go b/internal/devbox/refresh.go index dd44aeabc9a..9f9f650670b 100644 --- a/internal/devbox/refresh.go +++ b/internal/devbox/refresh.go @@ -26,7 +26,7 @@ func (d *Devbox) isGlobal() bool { // In some cases (e.g. 2 non-global projects somehow active at the same time), // refresh might not match. This is a tiny edge case, so no need to make UX // great, we just print out the entire command. -func (d *Devbox) refreshAliasOrCommand() string { +func (d *Devbox) RefreshAliasOrCommand() string { if !d.isRefreshAliasSet() { // even if alias is not set, it might still be set by the end of this process return fmt.Sprintf("`%s` or `%s`", d.refreshAliasName(), d.refreshCmd())