Skip to content

Conversation

ipince
Copy link
Contributor

@ipince ipince commented Nov 3, 2022

Summary

Devbox scripts allows you to define scripts to run inside a devbox shell. You can add them to your devbox.json like so:

{
  "packages": [],
  "shell": {
    "init_hook": "echo hook",
    "scripts": {
      "a": "echo hello",
      "b": [
        "echo good",
        "echo bye"
      ]
    }
  }
}

And then you can run with devbox run <script>. For example:

> devbox run b
Installing nix packages. This may take a while... done.
Starting a devbox shell...
hook
good
bye

Note that the shell's init hook will always run.

How was it tested?

With a devbox.json as written above and running devbox run <script> with no script, invalid script, valid script (single command and multi-command).

@ipince ipince requested a review from savil November 3, 2022 22:28
@ipince ipince marked this pull request as ready for review November 3, 2022 22:36
Copy link
Collaborator

@savil savil left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Approving to unblock but interested in discussion on the comments below

Use: "run <script>",
Hidden: true,
Short: "Starts a new devbox shell and runs the target script",
Long: "Starts a new interactive shell and runs your target script in it. The shell will exit once your target script is completed or when it is terminated via CTRL-C. Scripts can be defined in your `devbox.json`",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're running a script command, does the shell need to be interactive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the point is that you can start a long-running process, like a server, and it's stays in the foreground so you can Ctrl+C out of it when you want.

boxcli/run.go Outdated
Comment on lines 77 to 82
script := ""
if len(args) == 1 {
script = args[0]
} else if len(args) > 1 {
return "", "", errors.Errorf("too many arguments (expected 1, got %d)", len(args))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead, would it make sense to set Args: cobra.MaximumNArgs(1), above when defining the cobra command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I guess.. the error messages from the cobra validators are pretty bad though, but I'll see if this is reasonable.

}

if devbox.IsDevboxShellEnabled() {
return errors.New("You are already in an active devbox shell.\nRun 'exit' before calling devbox run again. Shell inception is not supported yet.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how you added yet compared to the same error in shell.go :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol yeah

@@ -37,7 +37,8 @@ type Config struct {
// Shell configures the devbox shell environment.
Shell struct {
// InitHook contains commands that will run at shell startup.
InitHook ConfigShellCmds `json:"init_hook,omitempty"`
InitHook ConfigShellCmds `json:"init_hook,omitempty"`
Scripts map[string]*ConfigShellCmds `json:"scripts,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in launchpad.yaml, we had a similar undeterministic-ordering issue with the Services. We handled it by:

  1. defining it as Scripts []ConfigShellCmds and adding the "name" as a field in ConfigShellCmds
  2. adding custom marshal and unmarshal functions to have it print as a map with the key

Would you wanna try that? Could do as a followup. I feel it would be really nice to ensure determinism. With determinism, we won't get random diffs of devbox.json when we write to it for (other unrelated) reasons.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting, i'll see about it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do as a follow-up

config.go Outdated
Comment on lines 88 to 107
func (c *Config) validate() error {
for k := range c.Shell.Scripts {
if k == "" {
return errors.New("cannot have script with empty name in devbox.json")
}
}
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh boy, I ended up adding another validate function today, so will need to merge it. It should be fairly straightforward.

@loreto had requested me to see if we can use the cue validation instead, which I am yet to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I actually attempted to use cue for validation, but I couldn't find what I needed quickly enough so I gave up and added the validation function. I can merge it with yours

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

devbox.go Outdated
Comment on lines 243 to 297
if err := d.ensurePackagesAreInstalled(install); err != nil {
return err
}

plan := d.ShellPlan()

profileDir, err := d.profileDir()
if err != nil {
return err
}

nixShellFilePath := filepath.Join(d.srcDir, ".devbox/gen/shell.nix")
script := d.cfg.Shell.Scripts[scriptName]
if script == nil {
return errors.Errorf("unable to find a script with name %s", scriptName)
}

shell, err := nix.DetectShell(
nix.WithPlanInitHook(strings.Join(plan.ShellInitHook, "\n")),
nix.WithProfile(profileDir),
nix.WithHistoryFile(filepath.Join(d.srcDir, shellHistoryFile)),
nix.WithUserScript(scriptName, script.String()))

if err != nil {
fmt.Print(err)
shell = &nix.Shell{}
}

shell.UserInitHook = d.cfg.Shell.InitHook.String()
return shell.Run(nixShellFilePath)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh boyo boy. I thought this would reuse the implementation of box.Exec....
but I agree with your implementation more (as in, we should strive to emulate the devbox shell environment which box.Exec doesn't seem to).

So, there are three different implementations now:

  • this RunScript
  • box.Exec
  • the box.Shell (except this doesn't run a specific command, but starts an interactive shell)

I feel:

  1. box.Exec and RunScript should use the exact same implementation
  2. box.Shell should also be refactored to use the same core implementation as box.Exec and RunScript (which is your TODO above)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm ok I'm not too familiar with box.Exec, I can look into it. I'm new to this code, so this type of feedback is really useful.

@ipince ipince merged commit 0d8fb9d into main Nov 10, 2022
@ipince ipince deleted the rodrigo/scripts branch November 10, 2022 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants