Skip to content

Commit 0d8fb9d

Browse files
ipinceLagoja
andauthored
First pass at devbox scripts implementation (#254)
## 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). Co-authored-by: John Lago <[email protected]>
1 parent 4199b93 commit 0d8fb9d

File tree

11 files changed

+201
-7
lines changed

11 files changed

+201
-7
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ __pycache__/
2626

2727
# Java
2828
*.class
29-
*.jar
29+
*.jar

boxcli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func RootCmd() *cobra.Command {
3131
command.AddCommand(InitCmd())
3232
command.AddCommand(PlanCmd())
3333
command.AddCommand(RemoveCmd())
34+
command.AddCommand(RunCmd())
3435
command.AddCommand(ShellCmd())
3536
command.AddCommand(VersionCmd())
3637
command.AddCommand(genDocsCmd())

boxcli/run.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package boxcli
5+
6+
import (
7+
"os"
8+
"os/exec"
9+
"sort"
10+
11+
"github.com/pkg/errors"
12+
"github.com/spf13/cobra"
13+
"go.jetpack.io/devbox"
14+
"golang.org/x/exp/slices"
15+
)
16+
17+
type runCmdFlags struct {
18+
config configFlags
19+
}
20+
21+
func RunCmd() *cobra.Command {
22+
flags := runCmdFlags{}
23+
command := &cobra.Command{
24+
Use: "run <script>",
25+
Hidden: true,
26+
Short: "Starts a new devbox shell and runs the target script",
27+
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`",
28+
Args: cobra.MaximumNArgs(1),
29+
PersistentPreRunE: nixShellPersistentPreRunE,
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
return runScriptCmd(args, flags)
32+
},
33+
}
34+
35+
flags.config.register(command)
36+
37+
return command
38+
}
39+
40+
func runScriptCmd(args []string, flags runCmdFlags) error {
41+
path, script, err := parseScriptArgs(args, flags)
42+
if err != nil {
43+
return err
44+
}
45+
46+
// Check the directory exists.
47+
box, err := devbox.Open(path, os.Stdout)
48+
if err != nil {
49+
return errors.WithStack(err)
50+
}
51+
52+
// Validate script exists.
53+
scripts := box.ListScripts()
54+
sort.Slice(scripts, func(i, j int) bool { return scripts[i] < scripts[j] })
55+
if script == "" || !slices.Contains(scripts, script) {
56+
return errors.Errorf("no script found with name \"%s\". "+
57+
"Here's a list of the existing scripts in devbox.json: %v", script, scripts)
58+
}
59+
60+
if devbox.IsDevboxShellEnabled() {
61+
return errors.New("You are already in an active devbox shell.\nRun 'exit' before calling devbox run again. Shell inception is not supported yet.")
62+
}
63+
64+
err = box.RunScript(script)
65+
66+
var exitErr *exec.ExitError
67+
if errors.As(err, &exitErr) {
68+
return nil
69+
}
70+
return err
71+
}
72+
73+
func parseScriptArgs(args []string, flags runCmdFlags) (string, string, error) {
74+
path, err := configPathFromUser([]string{}, &flags.config)
75+
if err != nil {
76+
return "", "", err
77+
}
78+
79+
script := ""
80+
if len(args) == 1 {
81+
script = args[0]
82+
}
83+
84+
return path, script, nil
85+
}

config.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ type Config struct {
3737
// Shell configures the devbox shell environment.
3838
Shell struct {
3939
// InitHook contains commands that will run at shell startup.
40-
InitHook ConfigShellCmds `json:"init_hook,omitempty"`
40+
InitHook ConfigShellCmds `json:"init_hook,omitempty"`
41+
Scripts map[string]*ConfigShellCmds `json:"scripts,omitempty"`
4142
} `json:"shell,omitempty"`
4243

4344
// Nixpkgs specifies the repository to pull packages from
@@ -90,6 +91,10 @@ func upgradeConfig(cfg *Config, absFilePath string) error {
9091

9192
// WriteConfig saves a devbox config file.
9293
func WriteConfig(path string, cfg *Config) error {
94+
err := validateConfig(cfg)
95+
if err != nil {
96+
return err
97+
}
9398
return cuecfg.WriteFile(path, cfg)
9499
}
95100

@@ -289,14 +294,29 @@ func missingConfigError(path string, didCheckParents bool) error {
289294

290295
func validateConfig(cfg *Config) error {
291296

292-
fns := [](func(cfg *Config) error){validateNixpkg}
297+
fns := [](func(cfg *Config) error){
298+
validateNixpkg,
299+
validateScripts,
300+
}
301+
293302
for _, fn := range fns {
294303
if err := fn(cfg); err != nil {
295304
return err
296305
}
297306
}
298307
return nil
299308
}
309+
func validateScripts(cfg *Config) error {
310+
for k := range cfg.Shell.Scripts {
311+
if strings.TrimSpace(k) == "" {
312+
return errors.New("cannot have script with empty name in devbox.json")
313+
}
314+
if strings.TrimSpace(cfg.Shell.Scripts[k].String()) == "" {
315+
return errors.New("cannot have an empty script value in devbox.json")
316+
}
317+
}
318+
return nil
319+
}
300320

301321
func validateNixpkg(cfg *Config) error {
302322
if cfg.Nixpkgs.Commit == "" {

cuecfg/cuecfg.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func ParseFile(path string, valuePtr any) error {
8181
return ParseFileWithExtension(path, filepath.Ext(path), valuePtr)
8282
}
8383

84-
// ParserFileWithExtension lets the caller override the extension of the `path` filename
84+
// ParseFileWithExtension lets the caller override the extension of the `path` filename
8585
// For example, project.csproj files should be treated as having extension .xml
8686
func ParseFileWithExtension(path string, ext string, valuePtr any) error {
8787
data, err := os.ReadFile(path)

devbox.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func (d *Devbox) Build(flags *docker.BuildFlags) error {
158158
return docker.Build(d.srcDir, opts...)
159159
}
160160

161-
// Plan creates a plan of the actions that devbox will take to generate its
161+
// ShellPlan creates a plan of the actions that devbox will take to generate its
162162
// shell environment.
163163
func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
164164
userDefinedPkgs := d.cfg.Packages
@@ -174,7 +174,7 @@ func (d *Devbox) ShellPlan() (*plansdk.ShellPlan, error) {
174174
return shellPlan, nil
175175
}
176176

177-
// Plan creates a plan of the actions that devbox will take to generate its
177+
// BuildPlan creates a plan of the actions that devbox will take to generate its
178178
// shell environment.
179179
func (d *Devbox) BuildPlan() (*plansdk.BuildPlan, error) {
180180
userPlan := d.convertToBuildPlan()
@@ -263,6 +263,52 @@ func (d *Devbox) Shell() error {
263263
return shell.Run(nixShellFilePath)
264264
}
265265

266+
// TODO: consider unifying the implementations of RunScript and Shell.
267+
func (d *Devbox) RunScript(scriptName string) error {
268+
if err := d.ensurePackagesAreInstalled(install); err != nil {
269+
return err
270+
}
271+
272+
plan, err := d.ShellPlan()
273+
if err != nil {
274+
return err
275+
}
276+
profileDir, err := d.profileDir()
277+
if err != nil {
278+
return err
279+
}
280+
281+
nixShellFilePath := filepath.Join(d.srcDir, ".devbox/gen/shell.nix")
282+
script := d.cfg.Shell.Scripts[scriptName]
283+
if script == nil {
284+
return errors.Errorf("unable to find a script with name %s", scriptName)
285+
}
286+
287+
shell, err := nix.DetectShell(
288+
nix.WithPlanInitHook(strings.Join(plan.ShellInitHook, "\n")),
289+
nix.WithProfile(profileDir),
290+
nix.WithHistoryFile(filepath.Join(d.srcDir, shellHistoryFile)),
291+
nix.WithUserScript(scriptName, script.String()))
292+
293+
if err != nil {
294+
fmt.Print(err)
295+
shell = &nix.Shell{}
296+
}
297+
298+
shell.UserInitHook = d.cfg.Shell.InitHook.String()
299+
return shell.Run(nixShellFilePath)
300+
}
301+
302+
func (d *Devbox) ListScripts() []string {
303+
keys := make([]string, len(d.cfg.Shell.Scripts))
304+
i := 0
305+
for k := range d.cfg.Shell.Scripts {
306+
keys[i] = k
307+
i++
308+
}
309+
return keys
310+
}
311+
266312
func (d *Devbox) Exec(cmds ...string) error {
267313
if err := d.ensurePackagesAreInstalled(install); err != nil {
268314
return err
@@ -371,7 +417,7 @@ func (d *Devbox) ensurePackagesAreInstalled(mode installMode) error {
371417
if mode == uninstall {
372418
installingVerb = "Uninstalling"
373419
}
374-
fmt.Fprintf(d.writer, "%s nix packages. This may take a while...", installingVerb)
420+
_, _ = fmt.Fprintf(d.writer, "%s nix packages. This may take a while... ", installingVerb)
375421

376422
// We need to re-install the packages
377423
if err := d.applyDevNixDerivation(); err != nil {

nix/shell.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ type Shell struct {
4545
// UserInitHook contains commands that will run at shell startup.
4646
UserInitHook string
4747

48+
ScriptName string
49+
ScriptCommand string
50+
4851
// profileDir is the absolute path to the directory storing the nix-profile
4952
profileDir string
5053
historyFile string
@@ -124,6 +127,13 @@ func WithEnvVariables(envVariables map[string]string) ShellOption {
124127
}
125128
}
126129

130+
func WithUserScript(name string, command string) ShellOption {
131+
return func(s *Shell) {
132+
s.ScriptName = name
133+
s.ScriptCommand = command
134+
}
135+
}
136+
127137
func WithPKGCOnfigDir(pkgConfigDir string) ShellOption {
128138
return func(s *Shell) {
129139
s.pkgConfigDir = pkgConfigDir
@@ -240,6 +250,10 @@ func (s *Shell) execCommand() string {
240250
}
241251
args = append(args, extraEnv...)
242252
args = append(args, s.binPath)
253+
if s.ScriptCommand != "" {
254+
args = append(args, "-ic")
255+
args = append(args, "run_script")
256+
}
243257
args = append(args, extraArgs...)
244258
return strings.Join(args, " ")
245259
}
@@ -296,13 +310,17 @@ func (s *Shell) writeDevboxShellrc() (path string, err error) {
296310
UserHook string
297311
PlanInitHook string
298312
PathPrepend string
313+
ScriptCommand string
314+
ProfileBinDir string
299315
HistoryFile string
300316
}{
301317
OriginalInit: string(bytes.TrimSpace(userShellrc)),
302318
OriginalInitPath: filepath.Clean(s.userShellrcPath),
303319
UserHook: strings.TrimSpace(s.UserInitHook),
304320
PlanInitHook: strings.TrimSpace(s.planInitHook),
305321
PathPrepend: pathPrepend,
322+
ScriptCommand: strings.TrimSpace(s.ScriptCommand),
323+
ProfileBinDir: s.profileDir + "/bin",
306324
HistoryFile: strings.TrimSpace(s.historyFile),
307325
})
308326
if err != nil {

nix/shellrc.tmpl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,15 @@ export PS1="(devbox) $PS1"
5858
{{- end }}
5959

6060
# End Plan Init Hook
61+
62+
# Begin Script command
63+
64+
{{- if .ScriptCommand }}
65+
66+
function run_script {
67+
{{ .ScriptCommand }}
68+
}
69+
70+
{{- end }}
71+
72+
# End Script command

nix/testdata/shellrc/basic/shellrc.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ echo "Hello from a devbox shell hook!"
6060
echo "Welcome to the devbox!"
6161

6262
# End Plan Init Hook
63+
64+
# Begin Script command
65+
66+
# End Script command

nix/testdata/shellrc/nohook/shellrc.golden

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ export PS1="(devbox) $PS1"
5454
echo "Welcome to the devbox!"
5555

5656
# End Plan Init Hook
57+
58+
# Begin Script command
59+
60+
# End Script command

0 commit comments

Comments
 (0)