diff --git a/cmd/bass/bump.go b/cmd/bass/bump.go index 504401c7..4bd18139 100644 --- a/cmd/bass/bump.go +++ b/cmd/bass/bump.go @@ -6,13 +6,14 @@ import ( "github.com/protocolbuffers/txtpbfmt/parser" "github.com/vito/bass/pkg/bass" + "github.com/vito/bass/pkg/cli" "github.com/vito/bass/pkg/proto" "github.com/vito/progrock" "google.golang.org/protobuf/encoding/prototext" ) func bump(ctx context.Context) error { - return withProgress(ctx, "export", func(ctx context.Context, vertex *progrock.VertexRecorder) error { + return cli.Task(ctx, cmdline, func(ctx context.Context, vertex *progrock.VertexRecorder) error { lockContent, err := os.ReadFile(bumpLock) if err != nil { return err diff --git a/cmd/bass/export.go b/cmd/bass/export.go index f9e8aa9b..1b563cc5 100644 --- a/cmd/bass/export.go +++ b/cmd/bass/export.go @@ -13,11 +13,12 @@ import ( "github.com/mattn/go-isatty" "github.com/tonistiigi/units" "github.com/vito/bass/pkg/bass" + "github.com/vito/bass/pkg/cli" "github.com/vito/progrock" ) func export(ctx context.Context) error { - return withProgress(ctx, "export", func(ctx context.Context, vertex *progrock.VertexRecorder) error { + return cli.Task(ctx, cmdline, func(ctx context.Context, vertex *progrock.VertexRecorder) error { dec := bass.NewRawDecoder(os.Stdin) var msg json.RawMessage diff --git a/cmd/bass/main.go b/cmd/bass/main.go index e66a6dd4..b3ef621b 100644 --- a/cmd/bass/main.go +++ b/cmd/bass/main.go @@ -8,6 +8,7 @@ import ( _ "net/http/pprof" "os" "runtime/pprof" + "strings" flag "github.com/spf13/pflag" "github.com/vito/bass/pkg/bass" @@ -18,6 +19,7 @@ import ( ) var flags = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) +var cmdline = strings.Join(os.Args, " ") var inputs []string @@ -140,15 +142,17 @@ func root(ctx context.Context) error { ctx = bass.WithRuntimePool(ctx, pool) if runnerAddr != "" { - return runnerLoop(ctx, runnerAddr, pool.Runtimes) + return cli.WithProgress(ctx, func(ctx context.Context) error { + return runnerLoop(ctx, pool.Runtimes) + }) } if runExport { - return export(ctx) + return cli.WithProgress(ctx, export) } if runPrune { - return prune(ctx) + return cli.WithProgress(ctx, prune) } if runLSP { @@ -156,26 +160,22 @@ func root(ctx context.Context) error { } if bumpLock != "" { - return bump(ctx) + return cli.WithProgress(ctx, bump) } - argv := flags.Args() - - if len(argv) == 0 { + if flags.NArg() == 0 { return repl(ctx) } - return run(ctx, argv[0], argv[1:]...) + return cli.WithProgress(ctx, run) } func repl(ctx context.Context) error { - env := bass.ImportSystemEnv() - scope := bass.NewRunScope(bass.Ground, bass.RunState{ Dir: bass.NewHostDir("."), Stdin: bass.Stdin, Stdout: bass.Stdout, - Env: env, + Env: bass.ImportSystemEnv(), }) return cli.Repl(ctx, scope) diff --git a/cmd/bass/progress.go b/cmd/bass/progress.go deleted file mode 100644 index c23c9b9b..00000000 --- a/cmd/bass/progress.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/signal" - - "github.com/opencontainers/go-digest" - "github.com/vito/bass/pkg/bass" - "github.com/vito/bass/pkg/cli" - "github.com/vito/bass/pkg/ioctx" - "github.com/vito/bass/pkg/zapctx" - "github.com/vito/progrock" -) - -var fancy bool - -func init() { - fancy = os.Getenv("BASS_FANCY_TUI") != "" -} - -func withProgress(ctx context.Context, name string, f func(context.Context, *progrock.VertexRecorder) error) (err error) { - ctx, stop := signal.NotifyContext(ctx, os.Interrupt) - defer stop() - - origCtx := ctx - defer func() { - if err != nil { - cli.WriteError(origCtx, err) - } - }() - - statuses, recorder, err := electRecorder() - if err != nil { - return - } - - defer recorder.Stop() - - ctx = progrock.RecorderToContext(ctx, recorder) - - if statuses != nil { - defer cleanupRecorder() - - recorder.Display(stop, cli.ProgressUI, os.Stderr, statuses, fancy) - } - - bassVertex := recorder.Vertex(digest.Digest(name), fmt.Sprintf("bass %s", name)) - defer func() { bassVertex.Done(err) }() - - stderr := bassVertex.Stderr() - - // wire up logs to vertex - logger := bass.LoggerTo(stderr) - ctx = zapctx.ToContext(ctx, logger) - - // wire up stderr for (log), (debug), etc. - ctx = ioctx.StderrToContext(ctx, stderr) - - err = f(ctx, bassVertex) - if err != nil { - return - } - - return -} diff --git a/cmd/bass/prune.go b/cmd/bass/prune.go index ab00512c..fe8b7a73 100644 --- a/cmd/bass/prune.go +++ b/cmd/bass/prune.go @@ -5,11 +5,12 @@ import ( "fmt" "github.com/vito/bass/pkg/bass" + "github.com/vito/bass/pkg/cli" "github.com/vito/progrock" ) func prune(ctx context.Context) error { - return withProgress(ctx, "prune", func(ctx context.Context, vertex *progrock.VertexRecorder) error { + return cli.Task(ctx, cmdline, func(ctx context.Context, vertex *progrock.VertexRecorder) error { pool, err := bass.RuntimePoolFromContext(ctx) if err != nil { return err diff --git a/cmd/bass/run.go b/cmd/bass/run.go index 8349b610..effea46c 100644 --- a/cmd/bass/run.go +++ b/cmd/bass/run.go @@ -3,7 +3,6 @@ package main import ( "context" "os" - "path/filepath" "github.com/mattn/go-isatty" "github.com/vito/bass/pkg/bass" @@ -11,62 +10,24 @@ import ( "github.com/vito/progrock" ) -func run(ctx context.Context, filePath string, argv ...string) error { - args := []bass.Value{} - for _, arg := range argv { - args = append(args, bass.String(arg)) - } - - analogousThunk := bass.Thunk{ - Cmd: bass.ThunkCmd{ - Host: &bass.HostPath{ - ContextDir: filepath.Dir(filePath), - Path: bass.FileOrDirPath{ - File: &bass.FilePath{Path: filepath.Base(filePath)}, - }, - }, - }, - Args: args, - } - - return withProgress(ctx, analogousThunk.Cmdline(), func(ctx context.Context, bassVertex *progrock.VertexRecorder) (err error) { - isTerm := isatty.IsTerminal(os.Stdout.Fd()) - - if !isTerm { - defer func() { - // ensure a chained unix pipeline exits - if err != nil && !isTerm { - os.Stdout.Close() - } - }() - } +func run(ctx context.Context) error { + return cli.Task(ctx, cmdline, func(ctx context.Context, vtx *progrock.VertexRecorder) error { + isTty := isatty.IsTerminal(os.Stdout.Fd()) stdout := bass.Stdout - if isTerm { - stdout = bass.NewSink(bass.NewJSONSink("stdout vertex", bassVertex.Stdout())) + if isTty { + stdout = bass.NewSink(bass.NewJSONSink("stdout vertex", vtx.Stdout())) } - env := bass.ImportSystemEnv() - - stdin := bass.Stdin - if len(inputs) > 0 { - stdin = cli.InputsSource(inputs) - } + argv := flags.Args() - scope := bass.NewRunScope(bass.Ground, bass.RunState{ - Dir: bass.NewHostDir(filepath.Dir(filePath)), - Stdin: stdin, - Stdout: stdout, - Env: env, - }) + err := cli.Run(ctx, bass.ImportSystemEnv(), inputs, argv[0], argv[1:], stdout) - source := bass.NewHostPath(filepath.Dir(filePath), bass.ParseFileOrDirPath(filepath.Base(filePath))) - _, err = bass.EvalFile(ctx, scope, filePath, source) - if err != nil { - return + if !isTty { + // ensure a chained unix pipeline exits + os.Stdout.Close() } - err = bass.RunMain(ctx, scope, args...) - return + return err }) } diff --git a/cmd/bass/runner.go b/cmd/bass/runner.go index e58afb07..4b8ded5f 100644 --- a/cmd/bass/runner.go +++ b/cmd/bass/runner.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/cenkalti/backoff/v4" + "github.com/vito/bass/pkg/cli" "github.com/vito/bass/pkg/runtimes" "github.com/vito/bass/pkg/zapctx" "github.com/vito/progrock" @@ -30,15 +31,12 @@ var defaultKeys = []string{ "id_rsa", } -func runnerLoop(ctx context.Context, sshAddr string, assoc []runtimes.Assoc) error { - ctx, stop := signal.NotifyContext(ctx, os.Interrupt) - defer stop() - - return withProgress(ctx, "runner", func(ctx context.Context, bassVertex *progrock.VertexRecorder) (err error) { +func runnerLoop(ctx context.Context, assoc []runtimes.Assoc) error { + return cli.Task(ctx, cmdline, func(ctx context.Context, bassVertex *progrock.VertexRecorder) (err error) { exp := backoff.NewExponentialBackOff() exp.MaxElapsedTime = 0 // https://www.youtube.com/watch?v=6BtuqUX934U return backoff.Retry(func() error { - return runner(ctx, sshAddr, assoc) + return runner(ctx, runnerAddr, assoc) }, backoff.WithContext(exp, ctx)) }) } diff --git a/pkg/bass/cache_path.go b/pkg/bass/cache_path.go index fdbc9c1b..998d8b11 100644 --- a/pkg/bass/cache_path.go +++ b/pkg/bass/cache_path.go @@ -133,3 +133,17 @@ func (path CachePath) Extend(ext Path) (Path, error) { return extended, nil } + +func (value CachePath) Dir() CachePath { + cp := value + + if value.Path.Dir != nil { + parent := value.Path.Dir.Dir() + cp.Path = FileOrDirPath{Dir: &parent} + } else { + parent := value.Path.File.Dir() + cp.Path = FileOrDirPath{Dir: &parent} + } + + return cp +} diff --git a/pkg/bass/host_path.go b/pkg/bass/host_path.go index 52eb5664..7489379b 100644 --- a/pkg/bass/host_path.go +++ b/pkg/bass/host_path.go @@ -163,6 +163,20 @@ func (path HostPath) Open(context.Context) (io.ReadCloser, error) { return os.Open(realPath) } +func (value HostPath) Dir() HostPath { + cp := value + + if value.Path.Dir != nil { + parent := value.Path.Dir.Dir() + cp.Path = FileOrDirPath{Dir: &parent} + } else { + parent := value.Path.File.Dir() + cp.Path = FileOrDirPath{Dir: &parent} + } + + return cp +} + func (value HostPath) fpath() string { return filepath.Join(value.ContextDir, value.Path.FilesystemPath().FromSlash()) } diff --git a/pkg/bass/session.go b/pkg/bass/session.go index 0a591a2e..837fa398 100644 --- a/pkg/bass/session.go +++ b/pkg/bass/session.go @@ -17,7 +17,7 @@ type Session struct { // Root is the base level scope inherited by all modules. Root *Scope - modules map[string]*Scope + modules map[uint64]*Scope mutex sync.Mutex } @@ -25,7 +25,7 @@ type Session struct { func NewBass() *Session { return &Session{ Root: Ground, - modules: map[string]*Scope{}, + modules: map[uint64]*Scope{}, } } @@ -33,21 +33,12 @@ func NewBass() *Session { func NewSession(ground *Scope) *Session { return &Session{ Root: ground, - modules: map[string]*Scope{}, + modules: map[uint64]*Scope{}, } } -func (session *Session) Run(ctx context.Context, thunk Thunk) error { - _, err := session.run(ctx, thunk, true, io.Discard) - if err != nil { - return err - } - - return nil -} - -func (session *Session) Read(ctx context.Context, w io.Writer, thunk Thunk) error { - _, err := session.run(ctx, thunk, true, w) +func (session *Session) Run(ctx context.Context, thunk Thunk, state RunState) error { + _, err := session.run(ctx, thunk, state, true) if err != nil { return err } @@ -56,7 +47,7 @@ func (session *Session) Read(ctx context.Context, w io.Writer, thunk Thunk) erro } func (session *Session) Load(ctx context.Context, thunk Thunk) (*Scope, error) { - key, err := thunk.Hash() + key, err := thunk.HashKey() if err != nil { return nil, err } @@ -69,7 +60,7 @@ func (session *Session) Load(ctx context.Context, thunk Thunk) (*Scope, error) { return module, nil } - module, err = session.run(ctx, thunk, false, io.Discard) + module, err = session.run(ctx, thunk, thunk.RunState(io.Discard), false) if err != nil { return nil, err } @@ -81,16 +72,9 @@ func (session *Session) Load(ctx context.Context, thunk Thunk) (*Scope, error) { return module, nil } -func (session *Session) run(ctx context.Context, thunk Thunk, runMain bool, w io.Writer) (*Scope, error) { +func (session *Session) run(ctx context.Context, thunk Thunk, state RunState, runMain bool) (*Scope, error) { var module *Scope - state := RunState{ - Dir: nil, // set below - Stdout: NewSink(NewJSONSink(thunk.String(), w)), - Stdin: NewSource(NewInMemorySource(thunk.Stdin...)), - Env: thunk.Env, - } - if thunk.Cmd.Cmd != nil { cp := thunk.Cmd.Cmd state.Dir = NewFSDir(std.FS) diff --git a/pkg/bass/thunk.go b/pkg/bass/thunk.go index 2b9ed792..bcd5252d 100644 --- a/pkg/bass/thunk.go +++ b/pkg/bass/thunk.go @@ -176,7 +176,16 @@ func (thunk Thunk) Run(ctx context.Context) error { return runtime.Run(ctx, thunk) } else { - return Bass.Run(ctx, thunk) + return Bass.Run(ctx, thunk, thunk.RunState(io.Discard)) + } +} + +func (thunk Thunk) RunState(stdout io.Writer) RunState { + return RunState{ + Dir: thunk.Cmd.RunDir(), + Env: thunk.Env, + Stdin: NewSource(NewInMemorySource(thunk.Stdin...)), + Stdout: NewSink(NewJSONSink(thunk.String(), stdout)), } } @@ -191,7 +200,7 @@ func (thunk Thunk) Read(ctx context.Context, w io.Writer) error { return runtime.Read(ctx, w, thunk) } else { - return Bass.Read(ctx, w, thunk) + return Bass.Run(ctx, thunk, thunk.RunState(w)) } } @@ -488,7 +497,7 @@ func (thunk *Thunk) Platform() *Platform { // Hash returns a stable, non-cryptographic hash derived from the thunk. func (thunk Thunk) Hash() (string, error) { - hash, err := thunk.hash() + hash, err := thunk.HashKey() if err != nil { return "", err } @@ -498,7 +507,7 @@ func (thunk Thunk) Hash() (string, error) { // Avatar returns an ASCII art avatar derived from the thunk. func (wl Thunk) Avatar() (*invaders.Invader, error) { - hash, err := wl.hash() + hash, err := wl.HashKey() if err != nil { return nil, err } @@ -519,7 +528,7 @@ func (thunk Thunk) CachePath(ctx context.Context, dest string) (string, error) { return Cache(ctx, filepath.Join(dest, "thunk-outputs", hash), thunk) } -func (thunk Thunk) hash() (uint64, error) { +func (thunk Thunk) HashKey() (uint64, error) { msg, err := thunk.MarshalProto() if err != nil { return 0, err diff --git a/pkg/bass/thunk_types.go b/pkg/bass/thunk_types.go index a669f2c5..a5edb8df 100644 --- a/pkg/bass/thunk_types.go +++ b/pkg/bass/thunk_types.go @@ -5,6 +5,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/vito/bass/pkg/proto" + "github.com/vito/bass/std" ) // ThunkMount configures a mount for the thunk. @@ -594,6 +595,24 @@ func (cmd ThunkCmd) Inner() (Value, error) { } } +func (cmd ThunkCmd) RunDir() Path { + if cmd.File != nil { + return cmd.File.Dir() + } else if cmd.Thunk != nil { + return cmd.Thunk.Dir() + } else if cmd.Cmd != nil { + return NewFSDir(std.FS) + } else if cmd.Host != nil { + return cmd.Host.Dir() + } else if cmd.FS != nil { + return cmd.FS.Dir() + } else if cmd.Cache != nil { + return cmd.Cache.Dir() + } else { + panic(fmt.Sprintf("ThunkCmd.RunDir: no value present: %+v", cmd)) + } +} + func (path *ThunkCmd) UnmarshalJSON(payload []byte) error { return UnmarshalJSON(payload, path) } diff --git a/pkg/cli/progress.go b/pkg/cli/progress.go index 938a31c6..0e0ea6de 100644 --- a/pkg/cli/progress.go +++ b/pkg/cli/progress.go @@ -2,11 +2,18 @@ package cli import ( "bytes" + "context" "io" + "os" + "os/signal" "sync" "github.com/morikuni/aec" "github.com/opencontainers/go-digest" + "github.com/vito/bass/pkg/bass" + "github.com/vito/bass/pkg/ioctx" + "github.com/vito/bass/pkg/zapctx" + "github.com/vito/progrock" "github.com/vito/progrock/graph" "github.com/vito/progrock/ui" ) @@ -86,3 +93,58 @@ func (prog *Progress) Summarize(w io.Writer) { printed: map[digest.Digest]struct{}{}, }.printAll(w) } + +var fancy bool + +func init() { + fancy = os.Getenv("BASS_FANCY_TUI") != "" +} + +func WithProgress(ctx context.Context, f func(context.Context) error) (err error) { + ctx, stop := signal.NotifyContext(ctx, os.Interrupt) + defer stop() + + statuses, recorder, err := electRecorder() + if err != nil { + WriteError(ctx, err) + return + } + + ctx = progrock.RecorderToContext(ctx, recorder) + + if statuses != nil { + defer cleanupRecorder() + + recorder.Display(stop, ProgressUI, os.Stderr, statuses, fancy) + } + + err = f(ctx) + + recorder.Stop() + + if err != nil { + WriteError(ctx, err) + } + + return +} + +func Task(ctx context.Context, name string, f func(context.Context, *progrock.VertexRecorder) error) error { + recorder := progrock.RecorderFromContext(ctx) + + vtx := recorder.Vertex(digest.Digest(name), name) + + stderr := vtx.Stderr() + + // wire up logs to vertex + logger := bass.LoggerTo(stderr) + ctx = zapctx.ToContext(ctx, logger) + + // wire up stderr for (log), (debug), etc. + ctx = ioctx.StderrToContext(ctx, stderr) + + // run the task + err := f(ctx, vtx) + vtx.Done(err) + return err +} diff --git a/cmd/bass/progress_unix.go b/pkg/cli/progress_unix.go similarity index 98% rename from cmd/bass/progress_unix.go rename to pkg/cli/progress_unix.go index f6f4c4b1..858950a6 100644 --- a/cmd/bass/progress_unix.go +++ b/pkg/cli/progress_unix.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package main +package cli import ( "fmt" diff --git a/cmd/bass/progress_windows.go b/pkg/cli/progress_windows.go similarity index 95% rename from cmd/bass/progress_windows.go rename to pkg/cli/progress_windows.go index 10cb16f7..5074fd7f 100644 --- a/cmd/bass/progress_windows.go +++ b/pkg/cli/progress_windows.go @@ -1,4 +1,4 @@ -package main +package cli import ( "github.com/vito/progrock" diff --git a/pkg/cli/run.go b/pkg/cli/run.go new file mode 100644 index 00000000..de147981 --- /dev/null +++ b/pkg/cli/run.go @@ -0,0 +1,47 @@ +package cli + +import ( + "context" + "path/filepath" + + "github.com/vito/bass/pkg/bass" +) + +func Run(ctx context.Context, env *bass.Scope, inputs []string, filePath string, argv []string, stdout *bass.Sink) error { + ctx, runs := bass.TrackRuns(ctx) + + dir, base := filepath.Split(filePath) + + cmd := bass.NewHostPath( + dir, + bass.ParseFileOrDirPath(filepath.ToSlash(base)), + ) + + thunk := bass.Thunk{ + Cmd: bass.ThunkCmd{ + Host: &cmd, + }, + Env: env, + } + + for _, arg := range argv { + thunk.Args = append(thunk.Args, bass.String(arg)) + } + + stdin := bass.Stdin + if len(inputs) > 0 { + stdin = InputsSource(inputs) + } + + err := bass.NewBass().Run(ctx, thunk, bass.RunState{ + Dir: bass.NewHostDir(filepath.Dir(filePath)), + Stdin: stdin, + Stdout: stdout, + Env: thunk.Env, + }) + if err != nil { + return err + } + + return runs.Wait() +} diff --git a/pkg/cli/run_test.go b/pkg/cli/run_test.go new file mode 100644 index 00000000..032dad0f --- /dev/null +++ b/pkg/cli/run_test.go @@ -0,0 +1,101 @@ +package cli_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/vito/bass/pkg/bass" + "github.com/vito/bass/pkg/basstest" + "github.com/vito/bass/pkg/cli" + "github.com/vito/is" +) + +func TestRun(t *testing.T) { + for _, test := range []struct { + name string + env *bass.Scope + inputs []string + helpers map[string]string + script string + argv []string + stdout []bass.Value + err error + }{ + {name: "empty", script: ``}, + { + name: "main", + script: `(defn main [] (emit 42 *stdout*))`, + stdout: []bass.Value{bass.Int(42)}, + }, + { + name: "argv", + script: `(defn main [val] (emit val *stdout*))`, + argv: []string{"hello"}, + stdout: []bass.Value{bass.String("hello")}, + }, + { + name: "env", + script: `(defn main [] (emit *env*:FOO *stdout*))`, + env: bass.Bindings{"FOO": bass.String("hello")}.Scope(), + stdout: []bass.Value{bass.String("hello")}, + }, + { + name: "inputs", + script: `(defn main [] (for [params *stdin*] (emit params *stdout*)))`, + inputs: []string{"str=hello", "path=./foo"}, + stdout: []bass.Value{ + bass.Bindings{ + "str": bass.String("hello"), + "path": bass.NewHostPath(".", bass.ParseFileOrDirPath("./foo")), + }.Scope(), + }, + }, + { + name: "waiting on started thunks propagates errors", + helpers: map[string]string{ + "fail.bass": `(defn main [] (error "boom"))`, + }, + script: `(defn main [] (start (*dir*/fail.bass) (fn [err] (and err (err)))) (emit 42 *stdout*))`, + stdout: []bass.Value{bass.Int(42)}, + err: &bass.StructuredError{ + Message: "boom", + Fields: bass.NewEmptyScope(), + }, + }, + { + name: "waiting after using succeeds does not error", + helpers: map[string]string{ + "fail.bass": `(defn main [] (error "boom"))`, + }, + script: `(defn main [] (emit (succeeds? (*dir*/fail.bass)) *stdout*))`, + stdout: []bass.Value{bass.Bool(false)}, + }, + } { + t.Run(test.name, func(t *testing.T) { + is := is.New(t) + + tmp := t.TempDir() + script := filepath.Join(tmp, test.name+".bass") + is.NoErr(os.WriteFile(script, []byte(test.script), 0644)) + for fn, content := range test.helpers { + script := filepath.Join(tmp, fn) + is.NoErr(os.WriteFile(script, []byte(content), 0644)) + } + + stdout := bass.NewInMemorySink() + runErr := cli.Run(context.Background(), test.env, test.inputs, script, test.argv, bass.NewSink(stdout)) + if test.err != nil { + is.Equal(test.err, runErr) + } else { + is.NoErr(runErr) + } + + is.Equal(len(test.stdout), len(stdout.Values)) + for i, v := range test.stdout { + basstest.Equal(t, v, stdout.Values[i]) + } + }) + } +}