diff --git a/internal/chezmoi/lazy.go b/internal/chezmoi/lazy.go index 13df1f3214d..c7b871358f5 100644 --- a/internal/chezmoi/lazy.go +++ b/internal/chezmoi/lazy.go @@ -1,9 +1,23 @@ package chezmoi +import "os/exec" + +// A commandFunc is a function that returns an *os/exec.Cmd. +type commandFunc func() *exec.Cmd + // A contentsFunc is a function that returns the contents of a file or an error. // It is typically used for lazy evaluation of a file's contents. type contentsFunc func() ([]byte, error) +// A lazyCommand returns an *os/exec.Cmd lazily. It is needed to defer the call +// to os/exec.Command because os/exec.Command calls os/exec.LookupPath and +// therefore depends on the state of $PATH when os/exec.Command is called, not +// the state of $PATH when os/exec.Cmd.{Run,Start} is called. +type lazyCommand struct { + commandFunc commandFunc + command *exec.Cmd +} + // A lazyContents evaluates its contents lazily. type lazyContents struct { contentsFunc contentsFunc @@ -24,6 +38,22 @@ type lazyLinkname struct { linknameSHA256 []byte } +// newLazyCommandFunc returns a new lazyCommand with commandFunc. +func newLazyCommandFunc(commandFunc func() *exec.Cmd) *lazyCommand { + return &lazyCommand{ + commandFunc: commandFunc, + } +} + +// Command returns lc's command. +func (lc *lazyCommand) Command() *exec.Cmd { + if lc.commandFunc != nil { + lc.command = lc.commandFunc() + lc.commandFunc = nil + } + return lc.command +} + // newLazyContents returns a new lazyContents with contents. func newLazyContents(contents []byte) *lazyContents { return &lazyContents{ @@ -32,7 +62,7 @@ func newLazyContents(contents []byte) *lazyContents { } // newLazyContentsFunc returns a new lazyContents with contentsFunc. -func newLazyContentsFunc(contentsFunc func() ([]byte, error)) *lazyContents { +func newLazyContentsFunc(contentsFunc contentsFunc) *lazyContents { return &lazyContents{ contentsFunc: contentsFunc, } diff --git a/internal/chezmoi/sourcestate.go b/internal/chezmoi/sourcestate.go index 7f5794e6631..2ee3501c807 100644 --- a/internal/chezmoi/sourcestate.go +++ b/internal/chezmoi/sourcestate.go @@ -1132,15 +1132,17 @@ func (s *SourceState) Read(ctx context.Context, options *ReadOptions) error { switch _, err := s.system.Lstat(destAbsPath); { case errors.Is(err, fs.ErrNotExist): // FIXME add support for using builtin git - args := []string{"clone"} - args = append(args, external.Clone.Args...) - args = append(args, external.URL, destAbsPath.String()) - cmd := exec.Command("git", args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr sourceStateCommand := &SourceStateCommand{ - cmd: cmd, + cmd: newLazyCommandFunc(func() *exec.Cmd { + args := []string{"clone"} + args = append(args, external.Clone.Args...) + args = append(args, external.URL, destAbsPath.String()) + cmd := exec.Command("git", args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd + }), origin: external, forceRefresh: options.RefreshExternals == RefreshExternalsAlways, refreshPeriod: external.RefreshPeriod, @@ -1153,15 +1155,17 @@ func (s *SourceState) Read(ctx context.Context, options *ReadOptions) error { return err default: // FIXME add support for using builtin git - args := []string{"pull"} - args = append(args, external.Pull.Args...) - cmd := exec.Command("git", args...) - cmd.Dir = destAbsPath.String() - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr sourceStateCommand := &SourceStateCommand{ - cmd: cmd, + cmd: newLazyCommandFunc(func() *exec.Cmd { + args := []string{"pull"} + args = append(args, external.Pull.Args...) + cmd := exec.Command("git", args...) + cmd.Dir = destAbsPath.String() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd + }), origin: external, forceRefresh: options.RefreshExternals == RefreshExternalsAlways, refreshPeriod: external.RefreshPeriod, diff --git a/internal/chezmoi/sourcestateentry.go b/internal/chezmoi/sourcestateentry.go index 30aa2da83b6..60af2a235b3 100644 --- a/internal/chezmoi/sourcestateentry.go +++ b/internal/chezmoi/sourcestateentry.go @@ -2,7 +2,6 @@ package chezmoi import ( "encoding/hex" - "os/exec" "github.com/rs/zerolog" @@ -38,7 +37,7 @@ type SourceStateEntry interface { // A SourceStateCommand represents a command that should be run. type SourceStateCommand struct { - cmd *exec.Cmd + cmd *lazyCommand origin SourceStateOrigin forceRefresh bool refreshPeriod Duration @@ -96,7 +95,7 @@ func (s *SourceStateCommand) Evaluate() error { // MarshalZerologObject implements // github.com/rs/zerolog.LogObjectMarshaler.MarshalZerologObject. func (s *SourceStateCommand) MarshalZerologObject(e *zerolog.Event) { - e.EmbedObject(chezmoilog.OSExecCmdLogObject{Cmd: s.cmd}) + e.EmbedObject(chezmoilog.OSExecCmdLogObject{Cmd: s.cmd.Command()}) e.Str("origin", s.origin.OriginString()) } diff --git a/internal/chezmoi/targetstateentry.go b/internal/chezmoi/targetstateentry.go index 55121c5f9b9..65a3897e2cd 100644 --- a/internal/chezmoi/targetstateentry.go +++ b/internal/chezmoi/targetstateentry.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "io/fs" - "os/exec" "runtime" "time" ) @@ -26,7 +25,7 @@ type TargetStateEntry interface { // A TargetStateModifyDirWithCmd represents running a command that modifies // a directory. type TargetStateModifyDirWithCmd struct { - cmd *exec.Cmd + cmd *lazyCommand forceRefresh bool refreshPeriod Duration sourceAttr SourceAttr @@ -91,7 +90,7 @@ func (t *TargetStateModifyDirWithCmd) Apply( } runAt := time.Now().UTC() - if err := system.RunCmd(t.cmd); err != nil { + if err := system.RunCmd(t.cmd.Command()); err != nil { return false, fmt.Errorf("%s: %w", actualStateEntry.Path(), err) } diff --git a/internal/cmd/testdata/scripts/issue3510.txtar b/internal/cmd/testdata/scripts/issue3510.txtar new file mode 100644 index 00000000000..27d6bd4de71 --- /dev/null +++ b/internal/cmd/testdata/scripts/issue3510.txtar @@ -0,0 +1,21 @@ +[windows] skip 'UNIX only' + +expandenv $CHEZMOISOURCEDIR/.chezmoiexternal.toml + +# test that chezmoi apply does not cache the absence of git in $PATH at startup +exec chezmoi apply +stdout 'using newly-installed git' + +-- golden/git -- +#!/bin/sh + +echo "using newly-installed git" +-- home/user/.local/share/chezmoi/.chezmoiexternal.toml -- +[".dir"] + type = "git-repo" + url = "file://$WORK/repo" +-- home/user/.local/share/chezmoi/run_once_before_install-git.sh -- +#!/bin/sh + +mkdir -p $WORK/bin +install -m 755 $WORK/golden/git $WORK/bin