diff --git a/builtin/builtin.go b/builtin/builtin.go index fd60852f..f07ffa48 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -328,6 +328,9 @@ var ( }, "option::run": LookupByType{ Func: map[string]FuncLookup{ + "capture": FuncLookup{ + Params: []*parser.Field{}, + }, "readonlyRootfs": FuncLookup{ Params: []*parser.Field{}, }, diff --git a/codegen/chain.go b/codegen/chain.go index c8302c19..216a4295 100644 --- a/codegen/chain.go +++ b/codegen/chain.go @@ -349,13 +349,22 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa // to be in the context of a specific function run is in. case with.Expr.FuncLit != nil: for _, stmt := range with.Expr.FuncLit.Body.NonEmptyStmts() { - if stmt.Call.Func.Name() != "mount" || stmt.Call.Alias == nil { + if stmt.Call.Alias == nil { continue } - target, err := cg.EmitStringExpr(ctx, scope, stmt.Call.Args[1]) - if err != nil { - return fc, err + var target string + switch stmt.Call.Func.Name() { + case "mount": + target, err = cg.EmitStringExpr(ctx, scope, stmt.Call.Args[1]) + if err != nil { + return fc, err + } + case "capture": + target = "capture" + } + if target == "" { + continue } calls[target] = stmt.Call @@ -381,7 +390,40 @@ func (cg *CodeGen) EmitFilesystemBuiltinChainStmt(ctx context.Context, scope *pa for _, target := range targets { // Mounts are unique by its mountpoint, and its vertex representing the // mount after execing can be aliased. - cont := ac(calls[target], exec.GetMount(target)) + var cont bool + switch calls[target].Func.Name() { + case "mount": + cont = ac(calls[target], exec.GetMount(target)) + case "capture": + cont = ac(calls[target], func() (string, error) { + st := exec.Root() + pw := cg.mw.WithPrefix("", false) + + s, err := cg.newSession(ctx) + if err != nil { + return "", err + } + + g, ctx := errgroup.WithContext(ctx) + + g.Go(func() error { + return s.Run(ctx, cg.cln.Dialer()) + }) + + var captureBuf strings.Builder + g.Go(func() error { + opts, err := cg.SolveOptions(ctx, st) + opts = append(opts, solver.WithOutputCapture(&captureBuf)) + def, err := st.Marshal(ctx, llb.LinuxAmd64) + if err != nil { + return err + } + return solver.Solve(ctx, cg.cln, s, pw, def, opts...) + }) + err = g.Wait() + return captureBuf.String(), err + }) + } if !cont { return exec.Root(), ErrAliasReached } @@ -940,11 +982,13 @@ func (cg *CodeGen) EmitStringChainStmt(ctx context.Context, scope *parser.Scope, return nil, err } return func(_ string) (string, error) { - str, ok := v.(string) - if !ok { - return str, errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast}) + switch s := v.(type) { + case string: + return s, nil + case func() (string, error): + return s() } - return str, nil + return "", errors.WithStack(ErrCodeGen{obj.Node, ErrBadCast}) }, nil } } diff --git a/codegen/codegen.go b/codegen/codegen.go index eec6ca91..9d49725a 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -1075,6 +1075,8 @@ func (cg *CodeGen) EmitExecOptions(ctx context.Context, scope *parser.Scope, op opts = append(opts, llb.Security(securityMode)) case "shlex": opts = append(opts, &shlexOption{}) + case "capture": + // no op, only relevant if aliased, handled in alias callback case "host": host, err := cg.EmitStringExpr(ctx, scope, args[0]) if err != nil { diff --git a/docs/reference.md b/docs/reference.md index 2ffdb2bd..d0ce4de1 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -480,6 +480,7 @@ If more than one arg is given, it will be executed directly, without a shell. #!hlb fs default() { run "arg" with option { + capture dir "path" env "key" "value" forward "src" "dest" @@ -497,6 +498,11 @@ If more than one arg is given, it will be executed directly, without a shell. } +#### option::run capture() + + + + #### option::run dir(string path) !!! info "string path" diff --git a/language/builtin.hlb b/language/builtin.hlb index e05b66a9..be7449fd 100644 --- a/language/builtin.hlb +++ b/language/builtin.hlb @@ -120,6 +120,8 @@ fs shell(variadic string arg) # @return the filesystem after the command has executed. fs run(variadic string arg) +option::run capture() + # Sets the rootfs as read-only for the duration of the run command. # # @return an option to set the rootfs as read-only. diff --git a/solver/solve.go b/solver/solve.go index 9b3b85f5..323d4aa3 100644 --- a/solver/solve.go +++ b/solver/solve.go @@ -3,6 +3,7 @@ package solver import ( "context" "encoding/json" + "io" "github.com/docker/buildx/util/progress" "github.com/moby/buildkit/client" @@ -26,6 +27,7 @@ type SolveInfo struct { Callbacks []func() error `json:"-"` ImageSpec *specs.Image Entitlements []entitlements.Entitlement + OutputCapture io.Writer } func WithDownloadDockerTarball(ref string) SolveOption { @@ -63,6 +65,13 @@ func WithDownloadOCITarball() SolveOption { } } +func WithOutputCapture(w io.Writer) SolveOption { + return func(info *SolveInfo) error { + info.OutputCapture = w + return nil + } +} + func WithCallback(fn func() error) SolveOption { return func(info *SolveInfo) error { info.Callbacks = append(info.Callbacks, fn) @@ -174,6 +183,36 @@ func Build(ctx context.Context, c *client.Client, s *session.Session, pw progres statusCh = pw.Status() } + if info.OutputCapture != nil { + captureStatusCh := make(chan *client.SolveStatus) + go func(origStatusCh chan *client.SolveStatus) { + defer func() { + if origStatusCh != nil { + close(origStatusCh) + } + }() + for { + select { + case <-pw.Done(): + return + case <-ctx.Done(): + return + case status, ok := <-captureStatusCh: + if !ok { + return + } + for _, log := range status.Logs { + info.OutputCapture.Write(log.Data) + } + if origStatusCh != nil { + origStatusCh <- status + } + } + } + }(statusCh) + statusCh = captureStatusCh + } + g.Go(func() error { _, err := c.Build(ctx, solveOpt, "", f, statusCh) return err