Skip to content

[fish] make init hook recursion fish compatible #1771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions internal/devbox/devbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,11 @@ func (d *Devbox) RunScript(ctx context.Context, cmdName string, cmdArgs []string
// which we don't want. So, one solution is to write the entire command and its arguments into the
// file itself, but that may not be great if the variables contain sensitive information. Instead,
// we save the entire command (with args) into the DEVBOX_RUN_CMD var, and then the script evals it.
err := shellgen.WriteScriptFile(d, arbitraryCmdFilename, shellgen.ScriptBody(d, "eval $DEVBOX_RUN_CMD\n"))
scriptBody, err := shellgen.ScriptBody(d, "eval $DEVBOX_RUN_CMD\n")
if err != nil {
return err
}
err = shellgen.WriteScriptFile(d, arbitraryCmdFilename, scriptBody)
if err != nil {
return err
}
Expand Down Expand Up @@ -325,13 +329,7 @@ func (d *Devbox) EnvExports(ctx context.Context, opts devopt.EnvExportsOpts) (st
envStr := exportify(envs)

if opts.RunHooks {
hooksFilename := shellgen.HooksFilename
if isFishShell() {
hooksFilename = shellgen.HooksFishFilename
}

hooksStr := ". " + shellgen.ScriptPath(d.ProjectDir(), hooksFilename)

hooksStr := ". " + shellgen.ScriptPath(d.ProjectDir(), shellgen.HooksFilename)
envStr = fmt.Sprintf("%s\n%s;\n", envStr, hooksStr)
}

Expand Down
4 changes: 1 addition & 3 deletions internal/devbox/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,8 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
}()

tmpl := shellrcTmpl
hooksFilePath := shellgen.ScriptPath(s.projectDir, shellgen.HooksFilename)
if s.name == shFish {
tmpl = fishrcTmpl
hooksFilePath = shellgen.ScriptPath(s.projectDir, shellgen.HooksFishFilename)
}

err = tmpl.Execute(shellrcf, struct {
Expand All @@ -333,7 +331,7 @@ func (s *DevboxShell) writeDevboxShellrc() (path string, err error) {
ProjectDir: s.projectDir,
OriginalInit: string(bytes.TrimSpace(userShellrc)),
OriginalInitPath: s.userShellrcPath,
HooksFilePath: hooksFilePath,
HooksFilePath: shellgen.ScriptPath(s.projectDir, shellgen.HooksFilename),
ShellStartTime: telemetry.FormatShellStart(s.shellStartTime),
HistoryFile: strings.TrimSpace(s.historyFile),
ExportEnv: exportify(s.env),
Expand Down
73 changes: 44 additions & 29 deletions internal/shellgen/scripts.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package shellgen

import (
"bytes"
"fmt"
"os"
"path/filepath"
Expand All @@ -18,20 +19,22 @@ import (
"go.jetpack.io/devbox/internal/plugin"
)

//go:embed tmpl/init-hook.tmpl
var initHookTmpl string
//go:embed tmpl/script-wrapper.tmpl
var scriptWrapperTmplString string
var scriptWrapperTmpl = template.Must(template.New("script-wrapper").Parse(scriptWrapperTmplString))

//go:embed tmpl/init-hook-fish.tmpl
var initHookFishTmpl string
//go:embed tmpl/init-hook-wrapper.tmpl
var initHookWrapperString string
var initHookWrapperTmpl = template.Must(template.New("init-hook-wrapper").Parse(initHookWrapperString))

const scriptsDir = ".devbox/gen/scripts"

// HooksFilename is the name of the file that contains the project's init-hooks and plugin hooks
const HooksFilename = ".hooks"

// This is only used in shellrc_fish.tmpl. A bit of a hack, because scripts use
// sh instead of fish.
const HooksFishFilename = ".hooks.fish"
// HooksFilename is the name of the file that contains a wrapper of the
// project's init-hooks and plugin hooks
const (
HooksFilename = ".hooks"
rawHooksFilename = ".raw-hooks"
)

type devboxer interface {
Config() *devconfig.Config
Expand Down Expand Up @@ -69,20 +72,25 @@ func WriteScriptsToFiles(devbox devboxer) error {
}
hooks := strings.Join(append(pluginHooks, devbox.Config().InitHook().String()), "\n\n")
// always write it, even if there are no hooks, because scripts will source it.
err = writeInitHookFile(devbox, hooks, initHookTmpl, HooksFilename)
err = writeRawInitHookFile(devbox, hooks)
if err != nil {
return errors.WithStack(err)
}
written[HooksFilename] = struct{}{}
err = writeInitHookFile(devbox, hooks, initHookFishTmpl, HooksFishFilename)
written[rawHooksFilename] = struct{}{}

err = writeInitHookWrapperFile(devbox)
if err != nil {
return errors.WithStack(err)
}
written[HooksFishFilename] = struct{}{}
written[HooksFilename] = struct{}{}

// Write scripts to files.
for name, body := range devbox.Config().Scripts() {
err = WriteScriptFile(devbox, name, ScriptBody(devbox, body.String()))
scriptBody, err := ScriptBody(devbox, body.String())
if err != nil {
return errors.WithStack(err)
}
err = WriteScriptFile(devbox, name, scriptBody)
if err != nil {
return errors.WithStack(err)
}
Expand All @@ -103,29 +111,27 @@ func WriteScriptsToFiles(devbox devboxer) error {
return nil
}

func writeInitHookFile(devbox devboxer, body, tmpl, filename string) (err error) {
script, err := createScriptFile(devbox, filename)
func writeRawInitHookFile(devbox devboxer, body string) (err error) {
script, err := createScriptFile(devbox, rawHooksFilename)
if err != nil {
return errors.WithStack(err)
}
defer script.Close() // best effort: close file

// skip adding init-hook recursion guard for the
// default hook or any empty hook
// there's nothing to guard, and it introduces complexity
if body == devconfig.DefaultInitHook || strings.TrimSpace(body) == "" {
_, err = script.WriteString(body)
return errors.WithStack(err)
}
_, err = script.WriteString(body)
return errors.WithStack(err)
}

t, err := template.New(filename).Parse(tmpl)
func writeInitHookWrapperFile(devbox devboxer) (err error) {
script, err := createScriptFile(devbox, HooksFilename)
if err != nil {
return errors.WithStack(err)
}
defer script.Close() // best effort: close file

return t.Execute(script, map[string]any{
"Body": body,
return initHookWrapperTmpl.Execute(script, map[string]string{
"InitHookHash": "__DEVBOX_INIT_HOOK_" + devbox.ProjectDirHash(),
"RawHooksFile": ScriptPath(devbox.ProjectDir(), rawHooksFilename),
})
}

Expand Down Expand Up @@ -167,6 +173,15 @@ func ScriptPath(projectDir, scriptName string) string {
return filepath.Join(projectDir, scriptsDir, scriptName+".sh")
}

func ScriptBody(d devboxer, body string) string {
return fmt.Sprintf(". %s\n\n%s", ScriptPath(d.ProjectDir(), HooksFilename), body)
func ScriptBody(d devboxer, body string) (string, error) {
var buf bytes.Buffer
err := scriptWrapperTmpl.Execute(&buf, map[string]string{
"Body": body,
"InitHookHash": "__DEVBOX_INIT_HOOK_" + d.ProjectDirHash(),
"InitHookPath": ScriptPath(d.ProjectDir(), HooksFilename),
})
if err != nil {
return "", errors.WithStack(err)
}
return buf.String(), nil
}
16 changes: 0 additions & 16 deletions internal/shellgen/tmpl/init-hook-fish.tmpl

This file was deleted.

9 changes: 9 additions & 0 deletions internal/shellgen/tmpl/init-hook-wrapper.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{{/*
Wrapping the hooks ensures that any script called within doesn't trigger more
init hooks.
Code here should be fish and POSIX compatible. That's why we use export to
remove the value
*/ -}}
export {{ .InitHookHash }}=true
. {{ .RawHooksFile }}
export {{ .InitHookHash }}=""
15 changes: 0 additions & 15 deletions internal/shellgen/tmpl/init-hook.tmpl

This file was deleted.

15 changes: 15 additions & 0 deletions internal/shellgen/tmpl/script-wrapper.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{/*
This wraps user scripts in devbox.json. The idea is to only run the init
hooks once, even if the init hook calls devbox run again. This will also
protect against using devbox service in the init hook.

Scripts always use sh to run, so POSIX is OK. We don't (yet) support fish
scripts. (though users can run a fish script within their script)
*/ -}}

if [ -z "${{ .InitHookHash }}" ]; then
{{/* init hooks will export InitHookHash ensuring no recursive sourcing*/ -}}
. {{ .InitHookPath }}
fi

{{ .Body }}